diff --git a/build.gradle b/build.gradle index 127d55e318..601e95229e 100644 --- a/build.gradle +++ b/build.gradle @@ -102,6 +102,9 @@ dependencies { implementation group: 'com.cronutils', name: 'cron-utils', version: '9.0.0' implementation 'io.grpc:grpc-netty-shaded:1.29.0' implementation group: 'com.google.protobuf', name: 'protobuf-java-util', version: '3.12.1' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.3' + implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.11.0' + testImplementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3' testImplementation group: 'com.googlecode.junit-toolbox', name: 'junit-toolbox', version: '2.4' diff --git a/src/main/java/io/temporal/activity/Activity.java b/src/main/java/io/temporal/activity/Activity.java index 0cc1000cde..1aa5973cb1 100644 --- a/src/main/java/io/temporal/activity/Activity.java +++ b/src/main/java/io/temporal/activity/Activity.java @@ -20,12 +20,13 @@ package io.temporal.activity; import io.temporal.client.ActivityCompletionException; +import io.temporal.failure.ActivityFailure; +import io.temporal.failure.ChildWorkflowFailure; +import io.temporal.failure.TimeoutFailure; import io.temporal.internal.sync.ActivityInternal; import io.temporal.internal.sync.WorkflowInternal; import io.temporal.proto.common.WorkflowExecution; import io.temporal.serviceclient.WorkflowServiceStubs; -import io.temporal.workflow.ActivityException; -import io.temporal.workflow.ActivityTimeoutException; import java.lang.reflect.Type; import java.util.Optional; @@ -234,7 +235,7 @@ public static ActivityTask getTask() { * Use to notify Temporal service that activity execution is alive. * * @param details In case of activity timeout can be accessed through {@link - * ActivityTimeoutException#getDetails(Class)} method. + * TimeoutFailure#getLastHeartbeatDetails()} method. * @throws ActivityCompletionException Indicates that activity execution is expected to be * interrupted. The reason for interruption is indicated by a type of subclass of the * exception. @@ -291,10 +292,10 @@ public static String getNamespace() { *

The reason for such design is that returning originally thrown exception from a remote call * (which child workflow and activity invocations are ) would not allow adding context information * about a failure, like activity and child workflow id. So stubs always throw a subclass of - * {@link ActivityException} from calls to an activity and subclass of {@link - * io.temporal.workflow.ChildWorkflowException} from calls to a child workflow. The original - * exception is attached as a cause to these wrapper exceptions. So as exceptions are always - * wrapped adding checked ones to method signature causes more pain than benefit. + * {@link ActivityFailure} from calls to an activity and subclass of {@link ChildWorkflowFailure} + * from calls to a child workflow. The original exception is attached as a cause to these wrapper + * exceptions. So as exceptions are always wrapped adding checked ones to method signature causes + * more pain than benefit. * *

Throws original exception if e is {@link RuntimeException} or {@link Error}. Never returns. * But return type is not empty to be able to use it as: diff --git a/src/main/java/io/temporal/activity/ActivityCancellationType.java b/src/main/java/io/temporal/activity/ActivityCancellationType.java index 784d95eff7..0fbb828c9b 100644 --- a/src/main/java/io/temporal/activity/ActivityCancellationType.java +++ b/src/main/java/io/temporal/activity/ActivityCancellationType.java @@ -19,13 +19,13 @@ package io.temporal.activity; +import io.temporal.failure.CanceledFailure; import io.temporal.workflow.CancellationScope; -import java.util.concurrent.CancellationException; /** * Defines behaviour of the parent workflow when {@link CancellationScope} that wraps child workflow * execution request is cancelled. The result of the cancellation independently of the type is a - * {@link CancellationException} thrown from the child workflow method. + * {@link CanceledFailure} thrown from the child workflow method. */ public enum ActivityCancellationType { /** diff --git a/src/main/java/io/temporal/activity/ActivityOptions.java b/src/main/java/io/temporal/activity/ActivityOptions.java index fc8bb861c9..90c40acdd3 100644 --- a/src/main/java/io/temporal/activity/ActivityOptions.java +++ b/src/main/java/io/temporal/activity/ActivityOptions.java @@ -23,9 +23,9 @@ import io.temporal.common.MethodRetry; import io.temporal.common.RetryOptions; import io.temporal.common.context.ContextPropagator; +import io.temporal.failure.CanceledFailure; import java.time.Duration; import java.util.List; -import java.util.concurrent.CancellationException; /** Options used to configure how an activity is invoked. */ public final class ActivityOptions { @@ -145,9 +145,9 @@ public Builder setContextPropagators(List contextPropagators) } /** - * In case of an activity cancellation it fails with a {@link CancellationException}Exception. - * If this flag is set to false then the exception is thrown not immediately but only after an - * activity completes its cleanup. If true a CancellationException is thrown immediately and an + * In case of an activity cancellation it fails with a {@link CanceledFailure}. If this flag is + * set to false then the exception is thrown not immediately but only after an activity + * completes its cleanup. If true a {@link CanceledFailure} is thrown immediately and an * activity cancellation is going to happen in the background. */ public Builder setCancellationType(ActivityCancellationType cancellationType) { diff --git a/src/main/java/io/temporal/client/ActivityCompletionClient.java b/src/main/java/io/temporal/client/ActivityCompletionClient.java index d363412389..488a363a19 100644 --- a/src/main/java/io/temporal/client/ActivityCompletionClient.java +++ b/src/main/java/io/temporal/client/ActivityCompletionClient.java @@ -20,8 +20,8 @@ package io.temporal.client; import io.temporal.activity.Activity; +import io.temporal.failure.CanceledFailure; import io.temporal.proto.common.WorkflowExecution; -import java.util.concurrent.CancellationException; /** * Used to complete asynchronously activities that called {@link Activity#doNotCompleteOnReturn()}. @@ -52,7 +52,7 @@ void reportCancellation(WorkflowExecution execution, String activityId, V de /** * Warning: heartbeating by ids is not implemented yet. * - * @throws CancellationException if activity is cancelled. + * @throws CanceledFailure if activity is cancelled. */ void heartbeat(WorkflowExecution execution, String activityId, V details) throws ActivityCompletionException; diff --git a/src/main/java/io/temporal/client/ActivityCompletionException.java b/src/main/java/io/temporal/client/ActivityCompletionException.java index 6acfb2bc40..566498f1ba 100644 --- a/src/main/java/io/temporal/client/ActivityCompletionException.java +++ b/src/main/java/io/temporal/client/ActivityCompletionException.java @@ -20,10 +20,11 @@ package io.temporal.client; import io.temporal.activity.ActivityTask; +import io.temporal.failure.TemporalException; import io.temporal.proto.common.WorkflowExecution; -/** Base exception for all failures returned by an activity completion client. */ -public class ActivityCompletionException extends RuntimeException { +/** Base exception for all failures returned by an activity completion client. Do not extend! */ +public class ActivityCompletionException extends TemporalException { private final WorkflowExecution execution; @@ -32,9 +33,7 @@ public class ActivityCompletionException extends RuntimeException { private final String activityId; protected ActivityCompletionException(ActivityTask task) { - execution = task.getWorkflowExecution(); - activityType = task.getActivityType(); - activityId = task.getActivityId(); + this(task, null); } protected ActivityCompletionException(ActivityTask task, Throwable cause) { @@ -60,7 +59,7 @@ protected ActivityCompletionException(ActivityTask task, Throwable cause) { } protected ActivityCompletionException(String activityId, Throwable cause) { - super("ActivityId" + activityId, cause); + super("ActivityId=" + activityId, cause); this.execution = null; this.activityType = null; this.activityId = activityId; @@ -71,7 +70,7 @@ protected ActivityCompletionException(Throwable cause) { } protected ActivityCompletionException() { - super(); + super(null, null); execution = null; activityType = null; activityId = null; diff --git a/src/main/java/io/temporal/client/WorkflowClient.java b/src/main/java/io/temporal/client/WorkflowClient.java index 8eab736086..18aa875ffb 100644 --- a/src/main/java/io/temporal/client/WorkflowClient.java +++ b/src/main/java/io/temporal/client/WorkflowClient.java @@ -87,10 +87,9 @@ * If you need to wait for a workflow completion after an asynchronous start, maybe even from a * different process, the simplest way is to call the blocking version again. If {@link * WorkflowOptions#getWorkflowIdReusePolicy()} is not {@code AllowDuplicate} then instead of - * throwing {@link io.temporal.client.DuplicateWorkflowException}, it reconnects to an existing - * workflow and waits for its completion. The following example shows how to do this from a - * different process than the one that started the workflow. All this process needs is a {@code - * WorkflowId}. + * throwing {@link WorkflowExecutionAlreadyStarted}, it reconnects to an existing workflow and waits + * for its completion. The following example shows how to do this from a different process than the + * one that started the workflow. All this process needs is a {@code WorkflowId}. * *


  * FileProcessingWorkflow workflow = workflowClient.newWorkflowStub(FileProcessingWorkflow.class, workflowId);
diff --git a/src/main/java/io/temporal/client/WorkflowClientOptions.java b/src/main/java/io/temporal/client/WorkflowClientOptions.java
index dd298c1483..984eb1ea5f 100644
--- a/src/main/java/io/temporal/client/WorkflowClientOptions.java
+++ b/src/main/java/io/temporal/client/WorkflowClientOptions.java
@@ -21,7 +21,6 @@
 
 import io.temporal.common.context.ContextPropagator;
 import io.temporal.common.converter.DataConverter;
-import io.temporal.common.converter.GsonJsonDataConverter;
 import io.temporal.proto.query.QueryRejectCondition;
 import java.lang.management.ManagementFactory;
 import java.util.Arrays;
@@ -82,7 +81,7 @@ public Builder setNamespace(String namespace) {
      * Overrides a data converter implementation used serialize workflow and activity arguments and
      * results.
      *
-     * 

Default is {@link GsonJsonDataConverter} data converter. + *

Default is {@link DataConverter#getDefaultInstance()}. */ public Builder setDataConverter(DataConverter dataConverter) { this.dataConverter = Objects.requireNonNull(dataConverter); @@ -155,7 +154,7 @@ public WorkflowClientOptions validateAndBuildWithDefaults() { } return new WorkflowClientOptions( namespace == null ? DEFAULT_NAMESPACE : namespace, - dataConverter == null ? GsonJsonDataConverter.getInstance() : dataConverter, + dataConverter == null ? DataConverter.getDefaultInstance() : dataConverter, interceptors == null ? EMPTY_INTERCEPTOR_ARRAY : interceptors, name, contextPropagators == null ? EMPTY_CONTEXT_PROPAGATORS : contextPropagators, diff --git a/src/main/java/io/temporal/client/WorkflowException.java b/src/main/java/io/temporal/client/WorkflowException.java index dfa1aee22c..07db5aedc8 100644 --- a/src/main/java/io/temporal/client/WorkflowException.java +++ b/src/main/java/io/temporal/client/WorkflowException.java @@ -19,46 +19,28 @@ package io.temporal.client; +import io.temporal.failure.TemporalException; import io.temporal.proto.common.WorkflowExecution; -import io.temporal.workflow.ChildWorkflowException; +import java.util.Objects; import java.util.Optional; -/** - * Base exception for all workflow failures returned by an external client. Note that inside a - * workflow implementation child workflows throw subclasses of {@link ChildWorkflowException}. - */ -public class WorkflowException extends RuntimeException { +/** Base exception for all workflow failures. */ +public abstract class WorkflowException extends TemporalException { private final WorkflowExecution execution; private final Optional workflowType; - protected WorkflowException( - String message, WorkflowExecution execution, Optional workflowType, Throwable cause) { - super(getMessage(message, execution, workflowType), cause); - this.execution = execution; - this.workflowType = workflowType; + protected WorkflowException(WorkflowExecution execution, String workflowType, Throwable cause) { + super(getMessage(execution, workflowType), cause); + this.execution = Objects.requireNonNull(execution); + this.workflowType = Optional.ofNullable(workflowType); } - private static String getMessage( - String message, WorkflowExecution execution, Optional workflowType) { - StringBuilder result = new StringBuilder(); - if (message != null) { - result.append(message); - result.append(", "); - } - if (workflowType.isPresent()) { - result.append("WorkflowType=\""); - result.append(workflowType.get()); - } - if (execution != null) { - if (result.length() > 0) { - result.append("\", "); - } - result.append("WorkflowExecution=\""); - result.append(execution); - result.append("\""); - } - return result.toString(); + protected WorkflowException( + String message, WorkflowExecution execution, String workflowType, Throwable cause) { + super(message, cause); + this.execution = Objects.requireNonNull(execution); + this.workflowType = Optional.ofNullable(workflowType); } public WorkflowExecution getExecution() { @@ -68,4 +50,13 @@ public WorkflowExecution getExecution() { public Optional getWorkflowType() { return workflowType; } + + public static String getMessage(WorkflowExecution execution, String workflowType) { + return "workflowId='" + + execution.getWorkflowId() + + "', runId='" + + execution.getRunId() + + (workflowType == null ? "" : "', workflowType='" + workflowType + '\'') + + '}'; + } } diff --git a/src/main/java/io/temporal/client/DuplicateWorkflowException.java b/src/main/java/io/temporal/client/WorkflowExecutionAlreadyStarted.java similarity index 86% rename from src/main/java/io/temporal/client/DuplicateWorkflowException.java rename to src/main/java/io/temporal/client/WorkflowExecutionAlreadyStarted.java index 2459e28d70..f8bfcae241 100644 --- a/src/main/java/io/temporal/client/DuplicateWorkflowException.java +++ b/src/main/java/io/temporal/client/WorkflowExecutionAlreadyStarted.java @@ -20,7 +20,6 @@ package io.temporal.client; import io.temporal.proto.common.WorkflowExecution; -import java.util.Optional; /** * This exception is thrown in the following cases: @@ -50,10 +49,9 @@ * io.temporal.proto.common.WorkflowIdReusePolicy#AllowDuplicate} * */ -public final class DuplicateWorkflowException extends WorkflowException { - - public DuplicateWorkflowException( - WorkflowExecution execution, String workflowType, String message) { - super(message, execution, Optional.of(workflowType), null); +public final class WorkflowExecutionAlreadyStarted extends WorkflowException { + public WorkflowExecutionAlreadyStarted( + WorkflowExecution execution, String workflowType, Throwable cause) { + super(execution, workflowType, cause); } } diff --git a/src/main/java/io/temporal/client/WorkflowFailureException.java b/src/main/java/io/temporal/client/WorkflowFailedException.java similarity index 52% rename from src/main/java/io/temporal/client/WorkflowFailureException.java rename to src/main/java/io/temporal/client/WorkflowFailedException.java index 9247bb6493..fc05ef126f 100644 --- a/src/main/java/io/temporal/client/WorkflowFailureException.java +++ b/src/main/java/io/temporal/client/WorkflowFailedException.java @@ -19,41 +19,54 @@ package io.temporal.client; +import io.temporal.proto.common.RetryStatus; import io.temporal.proto.common.WorkflowExecution; -import java.util.Optional; /** * Indicates that a workflow failed. An original cause of the workflow failure can be retrieved * through {@link #getCause()}. */ -public final class WorkflowFailureException extends WorkflowException { +public final class WorkflowFailedException extends WorkflowException { + private final RetryStatus retryStatus; private final long decisionTaskCompletedEventId; - public WorkflowFailureException( - WorkflowExecution execution, - Optional workflowType, + public WorkflowFailedException( + WorkflowExecution workflowExecution, + String workflowType, long decisionTaskCompletedEventId, - Throwable failure) { - super(getMessage(execution, workflowType), execution, workflowType, failure); + RetryStatus retryStatus, + Throwable cause) { + super( + getMessage(workflowExecution, workflowType, decisionTaskCompletedEventId, retryStatus), + workflowExecution, + workflowType, + cause); + this.retryStatus = retryStatus; this.decisionTaskCompletedEventId = decisionTaskCompletedEventId; } - private static String getMessage(WorkflowExecution execution, Optional workflowType) { - StringBuilder result = new StringBuilder(); - if (workflowType.isPresent()) { - result.append("WorkflowType=\""); - result.append(workflowType.get()); - result.append("\", "); - } - result.append("WorkflowId=\""); - result.append(execution.getWorkflowId()); - result.append("\", RunId=\""); - result.append(execution.getRunId()); - return result.toString(); + public RetryStatus getRetryStatus() { + return retryStatus; } public long getDecisionTaskCompletedEventId() { return decisionTaskCompletedEventId; } + + public static String getMessage( + WorkflowExecution workflowExecution, + String workflowType, + long decisionTaskCompletedEventId, + RetryStatus retryStatus) { + return "workflowId='" + + workflowExecution.getWorkflowId() + + "', runId='" + + workflowExecution.getRunId() + + (workflowType == null ? "'" : "', workflowType='" + workflowType + '\'') + + ", retryStatus=" + + retryStatus + + ", decisionTaskCompletedEventId=" + + decisionTaskCompletedEventId; + } } diff --git a/src/main/java/io/temporal/client/WorkflowNotFoundException.java b/src/main/java/io/temporal/client/WorkflowNotFoundException.java index 5beeca03cf..f00cef3f2d 100644 --- a/src/main/java/io/temporal/client/WorkflowNotFoundException.java +++ b/src/main/java/io/temporal/client/WorkflowNotFoundException.java @@ -20,7 +20,6 @@ package io.temporal.client; import io.temporal.proto.common.WorkflowExecution; -import java.util.Optional; /** * Thrown when workflow with the given id is not known to the Temporal service. It could be because @@ -28,8 +27,7 @@ */ public final class WorkflowNotFoundException extends WorkflowException { - public WorkflowNotFoundException( - WorkflowExecution execution, Optional workflowType, String message) { - super(message, execution, workflowType, null); + public WorkflowNotFoundException(WorkflowExecution execution, String workflowType) { + super(execution, workflowType, null); } } diff --git a/src/main/java/io/temporal/client/WorkflowQueryException.java b/src/main/java/io/temporal/client/WorkflowQueryException.java index 1a5d357aa9..fb6d91aa11 100644 --- a/src/main/java/io/temporal/client/WorkflowQueryException.java +++ b/src/main/java/io/temporal/client/WorkflowQueryException.java @@ -20,11 +20,10 @@ package io.temporal.client; import io.temporal.proto.common.WorkflowExecution; -import java.util.Optional; public class WorkflowQueryException extends WorkflowException { - public WorkflowQueryException(WorkflowExecution execution, String message) { - super(message, execution, Optional.empty(), null); + public WorkflowQueryException(WorkflowExecution execution, String workflowType, Throwable cause) { + super(execution, workflowType, cause); } } diff --git a/src/main/java/io/temporal/client/WorkflowQueryRejectedException.java b/src/main/java/io/temporal/client/WorkflowQueryRejectedException.java index 911ee34c85..81cb49f8e9 100644 --- a/src/main/java/io/temporal/client/WorkflowQueryRejectedException.java +++ b/src/main/java/io/temporal/client/WorkflowQueryRejectedException.java @@ -30,14 +30,11 @@ public final class WorkflowQueryRejectedException extends WorkflowQueryException public WorkflowQueryRejectedException( WorkflowExecution execution, + String workflowType, QueryRejectCondition queryRejectCondition, - WorkflowExecutionStatus workflowExecutionStatus) { - super( - execution, - "Query invoked with " - + queryRejectCondition - + " reject condition. The workflow execution status is " - + workflowExecutionStatus); + WorkflowExecutionStatus workflowExecutionStatus, + Throwable cause) { + super(execution, workflowType, cause); this.queryRejectCondition = queryRejectCondition; this.workflowExecutionStatus = workflowExecutionStatus; } diff --git a/src/main/java/io/temporal/client/WorkflowServiceException.java b/src/main/java/io/temporal/client/WorkflowServiceException.java index f40009ee69..fe9582c1cc 100644 --- a/src/main/java/io/temporal/client/WorkflowServiceException.java +++ b/src/main/java/io/temporal/client/WorkflowServiceException.java @@ -19,14 +19,11 @@ package io.temporal.client; -import io.temporal.internal.common.CheckedExceptionWrapper; import io.temporal.proto.common.WorkflowExecution; -import java.util.Optional; - -public final class WorkflowServiceException extends WorkflowException { +public class WorkflowServiceException extends WorkflowException { public WorkflowServiceException( - WorkflowExecution execution, Optional workflowType, Throwable failure) { - super(null, execution, workflowType, CheckedExceptionWrapper.unwrap(failure)); + WorkflowExecution execution, String workflowType, Throwable cause) { + super(execution, workflowType, cause); } } diff --git a/src/main/java/io/temporal/client/WorkflowTerminatedException.java b/src/main/java/io/temporal/client/WorkflowTerminatedException.java deleted file mode 100644 index 1cdc510391..0000000000 --- a/src/main/java/io/temporal/client/WorkflowTerminatedException.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. - * - * 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 io.temporal.client; - -import io.temporal.proto.common.WorkflowExecution; -import java.util.Optional; - -/** - * Indicates that a workflow was forcefully terminated by an external command to Temporal service. - */ -public final class WorkflowTerminatedException extends WorkflowException { - - private final byte[] details; - private final String identity; - - public WorkflowTerminatedException( - WorkflowExecution execution, - Optional workflowType, - String reason, - String identity, - byte[] details) { - super("Terminated by " + identity + " for \"" + reason + "\"", execution, workflowType, null); - this.identity = identity; - this.details = details; - } - - public String getIdentity() { - return identity; - } - - public byte[] getDetails() { - return details; - } -} diff --git a/src/main/java/io/temporal/client/WorkflowTimedOutException.java b/src/main/java/io/temporal/client/WorkflowTimedOutException.java deleted file mode 100644 index 4e622d36f2..0000000000 --- a/src/main/java/io/temporal/client/WorkflowTimedOutException.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. - * - * 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 io.temporal.client; - -import io.temporal.proto.common.TimeoutType; -import io.temporal.proto.common.WorkflowExecution; -import java.util.Optional; - -/** - * Indicates that a workflow exceeded its execution timeout and was forcefully terminated by the - * Temporal service. - */ -public final class WorkflowTimedOutException extends WorkflowException { - - private final TimeoutType timeoutType; - - public WorkflowTimedOutException( - WorkflowExecution execution, Optional workflowType, TimeoutType timeoutType) { - super(timeoutType + " timeout type", execution, workflowType, null); - this.timeoutType = timeoutType; - } - - public TimeoutType getTimeoutType() { - return timeoutType; - } -} diff --git a/src/main/java/io/temporal/common/MethodRetry.java b/src/main/java/io/temporal/common/MethodRetry.java index 3b80f049e9..9a7848abac 100644 --- a/src/main/java/io/temporal/common/MethodRetry.java +++ b/src/main/java/io/temporal/common/MethodRetry.java @@ -20,6 +20,7 @@ package io.temporal.common; import io.temporal.activity.ActivityOptions; +import io.temporal.failure.ApplicationFailure; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -64,12 +65,8 @@ long maximumIntervalSeconds() default 0; /** - * List of exceptions to retry. When matching an exact match is used. So adding - * RuntimeException.class to this list is going to include only RuntimeException itself, not all - * of its subclasses. The reason for such behaviour is to be able to support server side retries - * without knowledge of Java exception hierarchy. {@link Error} and {@link - * java.util.concurrent.CancellationException} are never retried, so they are not allowed in this - * list. + * List of failure types to not retry. The failure type of an exception is its full class name. It + * can be also explicitly specified by throwing an {@link ApplicationFailure} */ - Class[] doNotRetry() default {}; + String[] doNotRetry() default {}; } diff --git a/src/main/java/io/temporal/common/RetryOptions.java b/src/main/java/io/temporal/common/RetryOptions.java index 8f3740147e..35716e657e 100644 --- a/src/main/java/io/temporal/common/RetryOptions.java +++ b/src/main/java/io/temporal/common/RetryOptions.java @@ -20,12 +20,12 @@ package io.temporal.common; import com.google.common.base.Defaults; -import io.temporal.workflow.ActivityFailureException; -import io.temporal.workflow.ChildWorkflowFailureException; +import io.temporal.failure.ActivityFailure; +import io.temporal.failure.ApplicationFailure; +import io.temporal.failure.CanceledFailure; +import io.temporal.failure.ChildWorkflowFailure; import java.time.Duration; import java.util.Arrays; -import java.util.Collections; -import java.util.List; import java.util.Objects; import java.util.Optional; @@ -108,7 +108,7 @@ public RetryOptions merge(RetryOptions o) { } @SafeVarargs - public final RetryOptions addDoNotRetry(Class... doNotRetry) { + public final RetryOptions addDoNotRetry(String... doNotRetry) { if (doNotRetry == null) { return this; } @@ -123,7 +123,7 @@ public final RetryOptions addDoNotRetry(Class... doNotRetry .setInitialInterval(getInitialInterval()) .setMaximumInterval(getMaximumInterval()) .setBackoffCoefficient(backoffCoefficient) - .setDoNotRetry(merge(getDoNotRetry(), Arrays.asList(doNotRetry))); + .setDoNotRetry(merge(doNotRetry, getDoNotRetry())); if (getMaximumAttempts() > 0) { builder.setMaximumAttempts(getMaximumAttempts()); @@ -143,7 +143,7 @@ public static final class Builder { private Duration maximumInterval; - private List> doNotRetry; + private String[] doNotRetry; private Builder(RetryOptions options) { if (options == null) { @@ -206,20 +206,16 @@ public Builder setMaximumInterval(Duration maximumInterval) { } /** - * List of exceptions to retry. When matching an exact match is used. So adding - * RuntimeException.class to this list is going to include only RuntimeException itself, not all - * of its subclasses. The reason for such behaviour is to be able to support server side retries - * without knowledge of Java exception hierarchy. When considering an exception type a cause of - * {@link io.temporal.workflow.ActivityFailureException} and {@link - * io.temporal.workflow.ChildWorkflowFailureException} is looked at. + * List of exceptions application failures types to retry. Application failures are converted to + * {@link ApplicationFailure#getType()}. * - *

{@link Error} and {@link java.util.concurrent.CancellationException} are never retried and - * are not even passed to this filter. + *

{@link Error} and {@link CanceledFailure} are never retried and are not even passed to + * this filter. */ @SafeVarargs - public final Builder setDoNotRetry(Class... doNotRetry) { + public final Builder setDoNotRetry(String... doNotRetry) { if (doNotRetry != null) { - this.doNotRetry = Arrays.asList(doNotRetry); + this.doNotRetry = doNotRetry; } return this; } @@ -246,7 +242,7 @@ public RetryOptions validateBuildWithDefaults() { backoff, maximumAttempts, maximumInterval, - doNotRetry); + doNotRetry == null ? new String[0] : doNotRetry); } } @@ -258,19 +254,19 @@ public RetryOptions validateBuildWithDefaults() { private final Duration maximumInterval; - private final List> doNotRetry; + private final String[] doNotRetry; private RetryOptions( Duration initialInterval, double backoffCoefficient, int maximumAttempts, Duration maximumInterval, - List> doNotRetry) { + String[] doNotRetry) { this.initialInterval = initialInterval; this.backoffCoefficient = backoffCoefficient; this.maximumAttempts = maximumAttempts; this.maximumInterval = maximumInterval; - this.doNotRetry = doNotRetry != null ? Collections.unmodifiableList(doNotRetry) : null; + this.doNotRetry = doNotRetry; } public Duration getInitialInterval() { @@ -293,7 +289,7 @@ public Duration getMaximumInterval() { * @return null if not configured. When merging with annotation it makes a difference. null means * use values from an annotation. Empty list means do not retry on anything. */ - public List> getDoNotRetry() { + public String[] getDoNotRetry() { return doNotRetry; } @@ -313,7 +309,7 @@ public String toString() { + ", maximumInterval=" + maximumInterval + ", doNotRetry=" - + doNotRetry + + Arrays.toString(doNotRetry) + '}'; } @@ -326,13 +322,17 @@ public boolean equals(Object o) { && maximumAttempts == that.maximumAttempts && Objects.equals(initialInterval, that.initialInterval) && Objects.equals(maximumInterval, that.maximumInterval) - && Objects.equals(doNotRetry, that.doNotRetry); + && Arrays.equals(doNotRetry, that.doNotRetry); } @Override public int hashCode() { return Objects.hash( - initialInterval, backoffCoefficient, maximumAttempts, maximumInterval, doNotRetry); + initialInterval, + backoffCoefficient, + maximumAttempts, + maximumInterval, + Arrays.hashCode(doNotRetry)); } private static G merge(G annotation, G options, Class type) { @@ -349,29 +349,11 @@ private static Duration merge(long aSeconds, Duration o) { return aSeconds == 0 ? null : Duration.ofSeconds(aSeconds); } - private static Class[] merge( - Class[] a, List> o) { - if (o != null) { - @SuppressWarnings("unchecked") - Class[] result = new Class[o.size()]; - return o.toArray(result); + private static String[] merge(String[] fromAnnotation, String[] fromOptions) { + if (fromOptions != null) { + return fromOptions; } - return a.length == 0 ? null : a; - } - - private Class[] merge( - List> o1, List> o2) { - if (o2 != null) { - @SuppressWarnings("unchecked") - Class[] result = new Class[o2.size()]; - return o2.toArray(result); - } - if (o1.size() > 0) { - @SuppressWarnings("unchecked") - Class[] result = new Class[o1.size()]; - return o1.toArray(result); - } - return null; + return fromAnnotation; } public long calculateSleepTime(long attempt) { @@ -386,12 +368,18 @@ public long calculateSleepTime(long attempt) { public boolean shouldRethrow( Throwable e, Optional expiration, long attempt, long elapsed, long sleepTime) { - if (e instanceof ActivityFailureException || e instanceof ChildWorkflowFailureException) { + String type; + if (e instanceof ActivityFailure || e instanceof ChildWorkflowFailure) { e = e.getCause(); } + if (e instanceof ApplicationFailure) { + type = ((ApplicationFailure) e).getType(); + } else { + type = e.getClass().getName(); + } if (doNotRetry != null) { - for (Class doNotRetry : doNotRetry) { - if (doNotRetry.equals(e.getClass())) { + for (String doNotRetry : doNotRetry) { + if (doNotRetry.equals(type)) { return true; } } diff --git a/src/main/java/io/temporal/common/converter/ByteArrayPayloadConverter.java b/src/main/java/io/temporal/common/converter/ByteArrayPayloadConverter.java new file mode 100644 index 0000000000..599426eaee --- /dev/null +++ b/src/main/java/io/temporal/common/converter/ByteArrayPayloadConverter.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.common.converter; + +import com.google.protobuf.ByteString; +import io.temporal.proto.common.Payload; +import java.lang.reflect.Type; +import java.util.Optional; + +public final class ByteArrayPayloadConverter implements PayloadConverter { + @Override + public String getEncodingType() { + return EncodingKeys.METADATA_ENCODING_RAW_NAME; + } + + @Override + public Optional toData(Object value) throws DataConverterException { + if (value instanceof byte[]) { + return Optional.of( + Payload.newBuilder() + .putMetadata(EncodingKeys.METADATA_ENCODING_KEY, EncodingKeys.METADATA_ENCODING_RAW) + .setData(ByteString.copyFrom((byte[]) value)) + .build()); + } + return Optional.empty(); + } + + @Override + @SuppressWarnings("unchecked") + public T fromData(Payload content, Class valueClass, Type valueType) + throws DataConverterException { + ByteString data = content.getData(); + if (valueClass != byte[].class) { + throw new IllegalArgumentException( + "Raw encoding can be deserialized only to a byte array. valueClass=" + + valueClass.getName()); + } + return (T) data.toByteArray(); + } +} diff --git a/src/main/java/io/temporal/common/converter/CustomThrowableTypeAdapter.java b/src/main/java/io/temporal/common/converter/CustomThrowableTypeAdapter.java deleted file mode 100644 index f83b932da1..0000000000 --- a/src/main/java/io/temporal/common/converter/CustomThrowableTypeAdapter.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. - * - * 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 io.temporal.common.converter; - -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; -import io.temporal.internal.common.DataConverterUtils; -import java.io.IOException; -import java.util.regex.Pattern; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -final class CustomThrowableTypeAdapter extends TypeAdapter { - private static final Logger log = LoggerFactory.getLogger(CustomThrowableTypeAdapter.class); - - /** Used to parse a stack trace line. */ - private static final String TRACE_ELEMENT_REGEXP = - "((?.*)\\.(?.*))\\(((?.*?)(:(?\\d+))?)\\)"; - - private static final Pattern TRACE_ELEMENT_PATTERN = Pattern.compile(TRACE_ELEMENT_REGEXP); - - private final Gson gson; - private final TypeAdapterFactory skipPast; - - CustomThrowableTypeAdapter(Gson gson, TypeAdapterFactory skipPast) { - this.gson = gson; - this.skipPast = skipPast; - } - - @Override - @SuppressWarnings("unchecked") - public void write(JsonWriter jsonWriter, T throwable) throws IOException { - // We want to serialize the throwable and its cause separately, so that if the throwable - // is serializable but the cause is not, we can still serialize them correctly (i.e. we - // serialize the throwable correctly and convert the cause to a data converter exception). - // If existing cause is not detached due to security policy then null is returned. - Throwable cause = DataConverterUtils.detachCause(throwable); - - JsonObject object; - try { - TypeAdapter exceptionTypeAdapter = - gson.getDelegateAdapter(skipPast, TypeToken.get(throwable.getClass())); - object = exceptionTypeAdapter.toJsonTree(throwable).getAsJsonObject(); - object.add("class", new JsonPrimitive(throwable.getClass().getName())); - String stackTrace = DataConverterUtils.serializeStackTrace(throwable); - object.add("stackTrace", new JsonPrimitive(stackTrace)); - } catch (Throwable e) { - // In case a throwable is not serializable, we will convert it to a data converter exception. - // The cause of the data converter exception will indicate why the serialization failed. On - // the other hand, if the non-serializable throwable contains a cause, we will add it to the - // suppressed exceptions list. - DataConverterException ee = - new DataConverterException("Failure serializing exception: " + throwable.toString(), e); - if (cause != null) { - ee.addSuppressed(cause); - cause = null; - } - - TypeAdapter exceptionTypeAdapter = - new CustomThrowableTypeAdapter<>(gson, skipPast); - object = exceptionTypeAdapter.toJsonTree(ee).getAsJsonObject(); - } - - if (cause != null) { - TypeAdapter causeTypeAdapter = new CustomThrowableTypeAdapter<>(gson, skipPast); - try { - object.add("cause", causeTypeAdapter.toJsonTree(cause)); - } catch (Throwable e) { - DataConverterException ee = - new DataConverterException("Failure serializing exception: " + cause.toString(), e); - ee.setStackTrace(cause.getStackTrace()); - object.add("cause", causeTypeAdapter.toJsonTree(ee)); - } - } - - TypeAdapter elementAdapter = gson.getAdapter(JsonElement.class); - elementAdapter.write(jsonWriter, object); - } - - @Override - public T read(JsonReader jsonReader) throws IOException { - TypeAdapter elementAdapter = gson.getAdapter(JsonElement.class); - JsonObject object = elementAdapter.read(jsonReader).getAsJsonObject(); - JsonElement classElement = object.get("class"); - if (classElement != null) { - String className = classElement.getAsString(); - Class classType; - try { - classType = Class.forName(className); - } catch (ClassNotFoundException e) { - throw new IOException("Cannot deserialize " + className + " exception", e); - } - if (!Throwable.class.isAssignableFrom(classType)) { - throw new IOException("Expected type that extends Throwable: " + className); - } - - StackTraceElement[] stackTrace = parseStackTrace(object); - // This is important. Initially I tried configuring ExclusionStrategy to not - // deserialize the stackTrace field. - // But it left it null, which caused Thread.setStackTrace implementation to become - // silent noop. - object.add("stackTrace", new JsonArray()); - TypeAdapter exceptionTypeAdapter = - gson.getDelegateAdapter(skipPast, TypeToken.get(classType)); - Throwable result = (Throwable) exceptionTypeAdapter.fromJsonTree(object); - result.setStackTrace(stackTrace); - @SuppressWarnings("unchecked") - T typedResult = (T) result; - return typedResult; - } - throw new IOException(); - } - - private StackTraceElement[] parseStackTrace(JsonObject object) { - JsonElement jsonStackTrace = object.get("stackTrace"); - if (jsonStackTrace == null) { - return new StackTraceElement[0]; - } - String stackTrace = jsonStackTrace.getAsString(); - return DataConverterUtils.parseStackTrace(stackTrace); - } -} diff --git a/src/main/java/io/temporal/common/converter/DataConverter.java b/src/main/java/io/temporal/common/converter/DataConverter.java index ec894b880f..b3d1b1f8f6 100644 --- a/src/main/java/io/temporal/common/converter/DataConverter.java +++ b/src/main/java/io/temporal/common/converter/DataConverter.java @@ -19,6 +19,7 @@ package io.temporal.common.converter; +import io.temporal.proto.common.Payload; import io.temporal.proto.common.Payloads; import java.lang.reflect.Type; import java.util.Optional; @@ -31,7 +32,13 @@ */ public interface DataConverter { - PayloadConverter getPayloadConverter(); + static DataConverter getDefaultInstance() { + return DefaultDataConverter.getDefaultInstance(); + } + + Optional toPayload(T value); + + T fromPayload(Payload payload, Class valueClass, Type valueType); /** * Implements conversion of a list of values. @@ -41,7 +48,7 @@ public interface DataConverter { * @throws DataConverterException if conversion of the value passed as parameter failed for any * reason. */ - Optional toData(Object... values) throws DataConverterException; + Optional toPayloads(Object... values) throws DataConverterException; /** * Implements conversion of a single value. @@ -53,7 +60,7 @@ public interface DataConverter { * @throws DataConverterException if conversion of the data passed as parameter failed for any * reason. */ - T fromData(Optional content, Class parameterType, Type genericParameterType) + T fromPayloads(Optional content, Class parameterType, Type genericParameterType) throws DataConverterException; /** @@ -67,7 +74,7 @@ T fromData(Optional content, Class parameterType, Type genericP * @throws DataConverterException if conversion of the data passed as parameter failed for any * reason. */ - public Object[] fromDataArray( + Object[] arrayFromPayloads( Optional content, Class[] parameterTypes, Type[] genericParameterTypes) throws DataConverterException; } diff --git a/src/main/java/io/temporal/common/converter/DataConverterException.java b/src/main/java/io/temporal/common/converter/DataConverterException.java index 454bc3c570..c1eee624f9 100644 --- a/src/main/java/io/temporal/common/converter/DataConverterException.java +++ b/src/main/java/io/temporal/common/converter/DataConverterException.java @@ -60,6 +60,10 @@ public DataConverterException(String message, Optional content, Type[] super(toMessage(message, content, valueTypes)); } + public DataConverterException(Payload payload, Class valueClass, Throwable e) { + super(toMessage(e.getMessage(), payload, new Type[] {valueClass}), e); + } + private static String toMessage(String message, Optional content, Type[] valueTypes) { if (content == null && valueTypes == null || valueTypes.length == 0) { return message; diff --git a/src/main/java/io/temporal/common/converter/GsonJsonDataConverter.java b/src/main/java/io/temporal/common/converter/DefaultDataConverter.java similarity index 53% rename from src/main/java/io/temporal/common/converter/GsonJsonDataConverter.java rename to src/main/java/io/temporal/common/converter/DefaultDataConverter.java index 7b2b07cb59..d394e9491b 100644 --- a/src/main/java/io/temporal/common/converter/GsonJsonDataConverter.java +++ b/src/main/java/io/temporal/common/converter/DefaultDataConverter.java @@ -19,40 +19,92 @@ package io.temporal.common.converter; +import static java.nio.charset.StandardCharsets.UTF_8; + import com.google.common.base.Defaults; import io.temporal.proto.common.Payload; import io.temporal.proto.common.Payloads; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; /** - * Implements conversion through GSON JSON processor. To extend use {@link - * #GsonJsonDataConverter(PayloadConverter)} constructor. + * DataConverter that delegates conversion to type specific PayloadConverter instance. * * @author fateev */ -public final class GsonJsonDataConverter implements DataConverter { +public class DefaultDataConverter implements DataConverter { - private static final DataConverter INSTANCE = new GsonJsonDataConverter(); + private static final AtomicReference defaultDataConverterInstance = + new AtomicReference<>( + // Order is important as the first converter that can convert the payload is used + new DefaultDataConverter( + new NullPayloadConverter(), + new ByteArrayPayloadConverter(), + new JacksonJsonPayloadConverter())); private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; - private final PayloadConverter converter; + private final Map converterMap = new ConcurrentHashMap<>(); + private final List converters = new ArrayList<>(); - public static DataConverter getInstance() { - return INSTANCE; + static DataConverter getDefaultInstance() { + return defaultDataConverterInstance.get(); } - private GsonJsonDataConverter() { - this(GsonJsonPayloadConverter.getInstance()); + /** + * Override the global data converter default. Consider overriding data converter per client + * instance (using {@link + * io.temporal.client.WorkflowClientOptions.Builder#setDataConverter(DataConverter)} to avoid + * potential conflicts. + * + * @param converter + */ + public static void setDefaultDataConverter(DataConverter converter) { + defaultDataConverterInstance.set(converter); } - public GsonJsonDataConverter(PayloadConverter converter) { - this.converter = converter; + /** + * Creates instance from ordered array of converters. When converting an object to payload the + * array of converters is iterated from the beginning until one of the converters succesfully + * converts the value. + */ + public DefaultDataConverter(PayloadConverter... converters) { + for (PayloadConverter converter : converters) { + this.converters.add(converter); + this.converterMap.put(converter.getEncodingType(), converter); + } } @Override - public PayloadConverter getPayloadConverter() { - return converter; + public Optional toPayload(T value) { + for (PayloadConverter converter : converters) { + Optional result = converter.toData(value); + if (result.isPresent()) { + return result; + } + } + throw new IllegalArgumentException("Failure serializing " + value); + } + + @Override + public T fromPayload(Payload payload, Class valueClass, Type valueType) { + try { + String encoding = + payload.getMetadataOrThrow(EncodingKeys.METADATA_ENCODING_KEY).toString(UTF_8); + PayloadConverter converter = converterMap.get(encoding); + if (converter == null) { + throw new IllegalArgumentException("Unknown encoding: " + encoding); + } + return converter.fromData(payload, valueClass, valueType); + } catch (DataConverterException e) { + throw e; + } catch (Exception e) { + throw new DataConverterException(payload, valueClass, e); + } } /** @@ -63,14 +115,14 @@ public PayloadConverter getPayloadConverter() { * @return serialized values */ @Override - public Optional toData(Object... values) throws DataConverterException { + public Optional toPayloads(Object... values) throws DataConverterException { if (values == null || values.length == 0) { return Optional.empty(); } try { Payloads.Builder result = Payloads.newBuilder(); for (Object value : values) { - Optional payload = converter.toData(value); + Optional payload = toPayload(value); if (payload.isPresent()) { result.addPayloads(payload.get()); } else { @@ -86,7 +138,7 @@ public Optional toData(Object... values) throws DataConverterException } @Override - public T fromData(Optional content, Class valueClass, Type valueType) + public T fromPayloads(Optional content, Class valueClass, Type valueType) throws DataConverterException { if (!content.isPresent()) { return null; @@ -99,11 +151,11 @@ public T fromData(Optional content, Class valueClass, Type valu throw new DataConverterException( "Found multiple payloads while a single one expected", content, valueType); } - return converter.fromData(c.getPayloads(0), valueClass, valueType); + return fromPayload(c.getPayloads(0), valueClass, valueType); } @Override - public Object[] fromDataArray( + public Object[] arrayFromPayloads( Optional content, Class[] parameterTypes, Type[] valueTypes) throws DataConverterException { try { @@ -132,7 +184,7 @@ public Object[] fromDataArray( if (i >= count) { result[i] = Defaults.defaultValue((Class) vt); } else { - result[i] = converter.fromData(c.getPayloads(i), pt, vt); + result[i] = fromPayload(c.getPayloads(i), pt, vt); } } return result; diff --git a/src/main/java/io/temporal/common/converter/EncodedValue.java b/src/main/java/io/temporal/common/converter/EncodedValue.java new file mode 100644 index 0000000000..3e6bfbcc31 --- /dev/null +++ b/src/main/java/io/temporal/common/converter/EncodedValue.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.common.converter; + +import io.temporal.proto.common.Payloads; +import java.lang.reflect.Type; +import java.util.Objects; +import java.util.Optional; + +public final class EncodedValue implements Value { + private Optional payloads; + private DataConverter converter; + private final Optional value; + + public EncodedValue(Optional payloads, DataConverter converter) { + this.payloads = Objects.requireNonNull(payloads); + this.converter = converter; + this.value = null; + } + + public EncodedValue(T value) { + this.value = Optional.ofNullable(value); + this.payloads = null; + } + + public Optional toPayloads() { + if (payloads == null) { + if (converter == null) { + throw new IllegalStateException("converter not set"); + } + payloads = value.isPresent() ? converter.toPayloads(value.get()) : Optional.empty(); + } + return payloads; + } + + public void setDataConverter(DataConverter converter) { + this.converter = Objects.requireNonNull(converter); + } + + @Override + public T get(Class parameterType) throws DataConverterException { + if (value != null) { + @SuppressWarnings("unchecked") + T result = (T) value.orElse(null); + return result; + } else { + if (converter == null) { + throw new IllegalStateException("converter not set"); + } + return converter.fromPayloads(payloads, parameterType, parameterType); + } + } + + @Override + public T get(Class parameterType, Type genericParameterType) + throws DataConverterException { + return converter.fromPayloads(payloads, parameterType, genericParameterType); + } +} diff --git a/src/main/java/io/temporal/common/converter/EncodingKeys.java b/src/main/java/io/temporal/common/converter/EncodingKeys.java new file mode 100644 index 0000000000..f74929b426 --- /dev/null +++ b/src/main/java/io/temporal/common/converter/EncodingKeys.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.common.converter; + +import com.google.protobuf.ByteString; +import java.nio.charset.StandardCharsets; + +class EncodingKeys { + static final String METADATA_ENCODING_KEY = "encoding"; + static final String METADATA_ENCODING_NULL_NAME = "null"; + static final ByteString METADATA_ENCODING_NULL = + ByteString.copyFrom(METADATA_ENCODING_NULL_NAME, StandardCharsets.UTF_8); + + static final String METADATA_ENCODING_RAW_NAME = "raw"; + static final ByteString METADATA_ENCODING_RAW = + ByteString.copyFrom(METADATA_ENCODING_RAW_NAME, StandardCharsets.UTF_8); + static final String METADATA_ENCODING_JSON_NAME = "json"; + static final ByteString METADATA_ENCODING_JSON = + ByteString.copyFrom(METADATA_ENCODING_JSON_NAME, StandardCharsets.UTF_8); +} diff --git a/src/main/java/io/temporal/common/converter/GsonJsonPayloadConverter.java b/src/main/java/io/temporal/common/converter/GsonJsonPayloadConverter.java index 2dfdfd9e21..1812f9dd20 100644 --- a/src/main/java/io/temporal/common/converter/GsonJsonPayloadConverter.java +++ b/src/main/java/io/temporal/common/converter/GsonJsonPayloadConverter.java @@ -21,14 +21,8 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; import com.google.protobuf.ByteString; import io.temporal.proto.common.Payload; -import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.Optional; @@ -43,19 +37,6 @@ public final class GsonJsonPayloadConverter implements PayloadConverter { private static final PayloadConverter INSTANCE = new GsonJsonPayloadConverter(); - private static final String METADATA_ENCODING_KEY = "encoding"; - - private static final String METADATA_ENCODING_RAW_NAME = "raw"; - - private static final ByteString METADATA_ENCODING_RAW = - ByteString.copyFrom(METADATA_ENCODING_RAW_NAME, StandardCharsets.UTF_8); - private static final String METADATA_ENCODING_JSON_NAME = "json"; - - private static final ByteString METADATA_ENCODING_JSON = - ByteString.copyFrom(METADATA_ENCODING_JSON_NAME, StandardCharsets.UTF_8); - - private static final String TYPE_FIELD_NAME = "type"; - private static final String CLASS_NAME_FIELD_NAME = "className"; private final Gson gson; @@ -63,7 +44,7 @@ public static PayloadConverter getInstance() { return INSTANCE; } - private GsonJsonPayloadConverter() { + public GsonJsonPayloadConverter() { this((b) -> b); } @@ -73,35 +54,27 @@ private GsonJsonPayloadConverter() { * @param builderInterceptor function that intercepts {@link GsonBuilder} construction. */ public GsonJsonPayloadConverter(Function builderInterceptor) { - GsonBuilder gsonBuilder = - new GsonBuilder() - .serializeNulls() - .registerTypeAdapterFactory(new ThrowableTypeAdapterFactory()); + GsonBuilder gsonBuilder = new GsonBuilder().serializeNulls(); GsonBuilder intercepted = builderInterceptor.apply(gsonBuilder); gson = intercepted.create(); } + @Override + public String getEncodingType() { + return EncodingKeys.METADATA_ENCODING_JSON_NAME; + } + /** * Return empty if value is null. Exception stack traces are converted to a single string stack * trace to save space and make them more readable. */ @Override public Optional toData(Object value) throws DataConverterException { - if (value == null) { - return Optional.empty(); - } try { - if (value instanceof byte[]) { - return Optional.of( - Payload.newBuilder() - .putMetadata(METADATA_ENCODING_KEY, METADATA_ENCODING_RAW) - .setData(ByteString.copyFrom((byte[]) value)) - .build()); - } String json = gson.toJson(value); return Optional.of( Payload.newBuilder() - .putMetadata(METADATA_ENCODING_KEY, METADATA_ENCODING_JSON) + .putMetadata(EncodingKeys.METADATA_ENCODING_KEY, EncodingKeys.METADATA_ENCODING_JSON) .setData(ByteString.copyFrom(json, StandardCharsets.UTF_8)) .build()); } catch (DataConverterException e) { @@ -122,105 +95,10 @@ public T fromData(Payload content, Class valueClass, Type valueType) return null; } try { - String encoding = - content - .getMetadataOrDefault(METADATA_ENCODING_KEY, METADATA_ENCODING_JSON) - .toString(StandardCharsets.UTF_8); - if (METADATA_ENCODING_JSON_NAME.equals(encoding)) { - String json = data.toString(StandardCharsets.UTF_8); - return gson.fromJson(json, valueType); - } - if (METADATA_ENCODING_RAW_NAME.equals(encoding)) { - if (valueClass != byte[].class) { - throw new IllegalArgumentException( - "Raw encoding can be deserialized only to a byte array. valueClass=" - + valueClass.getName()); - } - return (T) data.toByteArray(); - } - throw new IllegalArgumentException("Unknown encoding type: " + encoding); + String json = data.toString(StandardCharsets.UTF_8); + return gson.fromJson(json, valueType); } catch (Exception e) { throw new DataConverterException(content, new Type[] {valueType}, e); } } - - /** - * Special handling of exception serialization and deserialization. Default JSON for stack traces - * is very space consuming and not readable by humans. So convert it into single text field and - * then parse it back into StackTraceElement array. - * - *

Implementation idea is based on https://github.com/google/gson/issues/43 - */ - private static class ThrowableTypeAdapterFactory implements TypeAdapterFactory { - - @Override - @SuppressWarnings("unchecked") - public TypeAdapter create(Gson gson, TypeToken typeToken) { - // Special handling of fields of DataConverter type. - // Needed to serialize exceptions like ActivityTimeoutException. - if (DataConverter.class.isAssignableFrom(typeToken.getRawType())) { - return new TypeAdapter() { - @Override - public void write(JsonWriter out, T value) throws IOException { - out.beginObject(); - out.name(TYPE_FIELD_NAME).value(METADATA_ENCODING_JSON_NAME); - out.endObject(); - } - - @Override - @SuppressWarnings("unchecked") - public T read(JsonReader in) throws IOException { - in.beginObject(); - if (!in.nextName().equals(TYPE_FIELD_NAME)) { - throw new IOException("Cannot deserialize DataConverter. Missing type field"); - } - String value = in.nextString(); - if (!METADATA_ENCODING_JSON_NAME.equals(value)) { - throw new IOException( - "Cannot deserialize DataConverter. Expected type is \"" - + METADATA_ENCODING_JSON_NAME - + "\". Found " - + value); - } - in.endObject(); - return (T) GsonJsonDataConverter.getInstance(); - } - }; - } - if (Class.class.isAssignableFrom(typeToken.getRawType())) { - return new TypeAdapter() { - @Override - public void write(JsonWriter out, T value) throws IOException { - out.beginObject(); - String className = ((Class) value).getName(); - out.name(CLASS_NAME_FIELD_NAME).value(className); - out.endObject(); - } - - @Override - public T read(JsonReader in) throws IOException { - in.beginObject(); - if (!in.nextName().equals(CLASS_NAME_FIELD_NAME)) { - throw new IOException( - "Cannot deserialize class. Missing " + CLASS_NAME_FIELD_NAME + " field"); - } - String className = in.nextString(); - try { - @SuppressWarnings("unchecked") - T result = (T) Class.forName(className); - in.endObject(); - return result; - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - }; - } - if (!Throwable.class.isAssignableFrom(typeToken.getRawType())) { - return null; // this class only serializes 'Throwable' and its subtypes - } - - return new CustomThrowableTypeAdapter(gson, this).nullSafe(); - } - } } diff --git a/src/main/java/io/temporal/common/converter/JacksonJsonPayloadConverter.java b/src/main/java/io/temporal/common/converter/JacksonJsonPayloadConverter.java new file mode 100644 index 0000000000..61a99549f5 --- /dev/null +++ b/src/main/java/io/temporal/common/converter/JacksonJsonPayloadConverter.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.common.converter; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.google.protobuf.ByteString; +import io.temporal.proto.common.Payload; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Optional; + +public class JacksonJsonPayloadConverter implements PayloadConverter { + + private final ObjectMapper mapper; + + public JacksonJsonPayloadConverter() { + mapper = new ObjectMapper(); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.registerModule(new JavaTimeModule()); + mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + } + + @Override + public String getEncodingType() { + return EncodingKeys.METADATA_ENCODING_JSON_NAME; + } + + @Override + public Optional toData(Object value) throws DataConverterException { + try { + byte[] serialized = mapper.writeValueAsBytes(value); + return Optional.of( + Payload.newBuilder() + .putMetadata(EncodingKeys.METADATA_ENCODING_KEY, EncodingKeys.METADATA_ENCODING_JSON) + .setData(ByteString.copyFrom(serialized)) + .build()); + + } catch (JsonProcessingException e) { + throw new DataConverterException(e); + } + } + + @Override + public T fromData(Payload content, Class valueClass, Type valueType) + throws DataConverterException { + ByteString data = content.getData(); + if (data.isEmpty()) { + return null; + } + try { + @SuppressWarnings("deprecation") + JavaType reference = mapper.getTypeFactory().constructType(valueType, valueClass); + return mapper.readValue(content.getData().toByteArray(), reference); + } catch (IOException e) { + throw new DataConverterException(e); + } + } +} diff --git a/src/main/java/io/temporal/common/converter/NullPayloadConverter.java b/src/main/java/io/temporal/common/converter/NullPayloadConverter.java new file mode 100644 index 0000000000..71669dc1a1 --- /dev/null +++ b/src/main/java/io/temporal/common/converter/NullPayloadConverter.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.common.converter; + +import io.temporal.proto.common.Payload; +import java.lang.reflect.Type; +import java.util.Optional; + +/** Encodes and decodes null values. */ +public final class NullPayloadConverter implements PayloadConverter { + @Override + public String getEncodingType() { + return EncodingKeys.METADATA_ENCODING_NULL_NAME; + } + + @Override + public Optional toData(Object value) throws DataConverterException { + if (value == null) { + return Optional.of( + Payload.newBuilder() + .putMetadata(EncodingKeys.METADATA_ENCODING_KEY, EncodingKeys.METADATA_ENCODING_NULL) + .build()); + } + return Optional.empty(); + } + + @Override + public T fromData(Payload content, Class valueClass, Type valueType) + throws DataConverterException { + return null; + } +} diff --git a/src/main/java/io/temporal/common/converter/PayloadConverter.java b/src/main/java/io/temporal/common/converter/PayloadConverter.java index e3b43df0c8..af2d6bcbfa 100644 --- a/src/main/java/io/temporal/common/converter/PayloadConverter.java +++ b/src/main/java/io/temporal/common/converter/PayloadConverter.java @@ -31,6 +31,8 @@ */ public interface PayloadConverter { + String getEncodingType(); + /** * Implements conversion of a list of values. * diff --git a/src/main/java/io/temporal/common/converter/Value.java b/src/main/java/io/temporal/common/converter/Value.java new file mode 100644 index 0000000000..5bee63f51a --- /dev/null +++ b/src/main/java/io/temporal/common/converter/Value.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.common.converter; + +import java.lang.reflect.Type; + +/** Value that can be extracted to an appropriate type. */ +public interface Value { + + /** + * Get value of the specified type. + * + * @param parameterType class of the value to get + * @param type of the value to get + * @return value or null + * @throws DataConverterException if value cannot be extracted to the given type + */ + T get(Class parameterType) throws DataConverterException; + + /** + * Get value of the specified generic type. For example if value is of type List use the + * following expression (using {@link com.google.common.reflect.TypeToken}) to extract: + * + *


+   * TypeToken<List<MyClass>> typeToken = new TypeToken<List<MyClass>>() {};
+   * List<MyClass> result = value.get(List.class, typeToken.getType());
+   *  
+ * + * @param parameterType class of the value to get + * @param genericParameterType the type of the value to get + * @param type of the value to get + * @return value or null + * @throws DataConverterException if value cannot be extracted to the given type + */ + T get(Class parameterType, Type genericParameterType) throws DataConverterException; +} diff --git a/src/main/java/io/temporal/failure/ActivityFailure.java b/src/main/java/io/temporal/failure/ActivityFailure.java new file mode 100644 index 0000000000..e4550c26ab --- /dev/null +++ b/src/main/java/io/temporal/failure/ActivityFailure.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.failure; + +import io.temporal.proto.common.RetryStatus; + +/** + * Contains information about an activity failure. Always contains the original reason for the + * failure as its cause. For example if an activity timed out the cause is {@link TimeoutFailure}. + * + *

This exception is expected to be thrown only by the framework code. + */ +public final class ActivityFailure extends TemporalFailure { + + private final long scheduledEventId; + private final long startedEventId; + private final String activityType; + private final String activityId; + private final String identity; + private final RetryStatus retryStatus; + + public ActivityFailure( + long scheduledEventId, + long startedEventId, + String activityType, + String activityId, + RetryStatus retryStatus, + String identity, + Throwable cause) { + super( + getMessage( + scheduledEventId, startedEventId, activityType, activityId, retryStatus, identity), + null, + cause); + this.scheduledEventId = scheduledEventId; + this.startedEventId = startedEventId; + this.activityType = activityType; + this.activityId = activityId; + this.identity = identity; + this.retryStatus = retryStatus; + } + + public long getScheduledEventId() { + return scheduledEventId; + } + + public long getStartedEventId() { + return startedEventId; + } + + public String getActivityType() { + return activityType; + } + + public String getActivityId() { + return activityId; + } + + public String getIdentity() { + return identity; + } + + public RetryStatus getRetryStatus() { + return retryStatus; + } + + public static String getMessage( + long scheduledEventId, + long startedEventId, + String activityType, + String activityId, + RetryStatus retryStatus, + String identity) { + return "scheduledEventId=" + + scheduledEventId + + ", startedEventId=" + + startedEventId + + ", activityType='" + + activityType + + '\'' + + (activityId == null ? "" : ", activityId='" + activityId + '\'') + + ", identity='" + + identity + + '\'' + + ", retryStatus=" + + retryStatus; + } +} diff --git a/src/main/java/io/temporal/failure/ApplicationFailure.java b/src/main/java/io/temporal/failure/ApplicationFailure.java new file mode 100644 index 0000000000..06af273e9b --- /dev/null +++ b/src/main/java/io/temporal/failure/ApplicationFailure.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.failure; + +import com.google.common.base.Strings; +import io.temporal.common.converter.DataConverter; +import io.temporal.common.converter.EncodedValue; +import io.temporal.common.converter.Value; + +/** + * Application failure is used to communicate application specific failures between workflows and + * activities. + * + *

Throw this exception to have full control over type and details if the exception delivered to + * the caller workflow or client. + * + *

Any unhandled exception which doesn't extend {@link TemporalFailure} is converted to an + * instance of this class before being returned to a caller. + * + *

The {@code type} property is used by {@link io.temporal.common.RetryOptions} to determine if + * an instance of this exception is non retryable. Another way to avoid retrying an exception of + * this type is by setting {@code nonRetryable} flag to @{code true}. + * + *

The conversion of an exception that doesn't extend {@link TemporalFailure} to an + * ApplicationFailure is done as following: + * + *

    + *
  • type is set to the exception full type name. + *
  • message is set to the exception message + *
  • nonRetryable is set to false + *
  • details are set to null + *
  • stack trace is copied from the original exception + *
+ */ +public final class ApplicationFailure extends TemporalFailure { + private final String type; + private final Value details; + private boolean nonRetryable; + + /** + * @param message optional error message + * @param type optional error type that is used by {@link + * io.temporal.common.RetryOptions#addDoNotRetry(String...)}. + * @param details optional details about the failure. They are serialized using the same approach + * as arguments and results and can be accessed through {@link #getDetails()} + * @param cause failure cause. Each element of the cause chain is converted to ApplicationFailure + * if it doesn't extend {@link TemporalFailure}. + */ + public ApplicationFailure(String message, String type, Object details, Exception cause) { + this(message, type, new EncodedValue(details), false, cause); + } + + /** + * @param message optional error message + * @param type optional error type that is used by {@link + * io.temporal.common.RetryOptions#addDoNotRetry(String...)}. + * @param details optional details about the failure. They are serialized using the same approach + * as arguments and results. + */ + public ApplicationFailure(String message, String type, Object details) { + this(message, type, new EncodedValue(details), false, null); + } + + /** + * @param message optional error message + * @param type optional error type that is used by {@link + * io.temporal.common.RetryOptions#addDoNotRetry(String...)}. + */ + public ApplicationFailure(String message, String type) { + this(message, type, new EncodedValue(null), false, null); + } + + /** * @param message optional error message */ + public ApplicationFailure(String message) { + this(message, null); + } + + ApplicationFailure( + String message, String type, Value details, boolean nonRetryable, Exception cause) { + super(getMessage(message, type, nonRetryable), message, cause); + this.type = type; + this.details = details; + this.nonRetryable = nonRetryable; + } + + public String getType() { + return type; + } + + public Value getDetails() { + return details; + } + + public void setNonRetryable(boolean nonRetryable) { + this.nonRetryable = nonRetryable; + } + + public boolean isNonRetryable() { + return nonRetryable; + } + + @Override + public void setDataConverter(DataConverter converter) { + ((EncodedValue) details).setDataConverter(converter); + } + + private static String getMessage(String message, String type, boolean nonRetryable) { + return (Strings.isNullOrEmpty(message) ? "" : "message='" + message + "\', ") + + "type='" + + type + + '\'' + + ", nonRetryable=" + + nonRetryable; + } +} diff --git a/src/main/java/io/temporal/internal/sync/SimulatedTimeoutExceptionInternal.java b/src/main/java/io/temporal/failure/CanceledFailure.java similarity index 52% rename from src/main/java/io/temporal/internal/sync/SimulatedTimeoutExceptionInternal.java rename to src/main/java/io/temporal/failure/CanceledFailure.java index 05973f6d5c..5533f30418 100644 --- a/src/main/java/io/temporal/internal/sync/SimulatedTimeoutExceptionInternal.java +++ b/src/main/java/io/temporal/failure/CanceledFailure.java @@ -17,35 +17,34 @@ * permissions and limitations under the License. */ -package io.temporal.internal.sync; +package io.temporal.failure; -import io.temporal.proto.common.TimeoutType; +import io.temporal.common.converter.DataConverter; +import io.temporal.common.converter.EncodedValue; +import io.temporal.common.converter.Value; -/** - * SimulatedTimeoutExceptionInternal is created from a SimulatedTimeoutException. The main - * difference is that the details are in a serialized form. - */ -final class SimulatedTimeoutExceptionInternal extends RuntimeException { - - private final TimeoutType timeoutType; - - private final byte[] details; +public final class CanceledFailure extends TemporalFailure { + private final Value details; - SimulatedTimeoutExceptionInternal(TimeoutType timeoutType, byte[] details) { - this.timeoutType = timeoutType; + public CanceledFailure(String message, Value details, Throwable cause) { + super(message, message, cause); this.details = details; } - SimulatedTimeoutExceptionInternal(TimeoutType timeoutType) { - this.timeoutType = timeoutType; - this.details = null; + public CanceledFailure(String message, Object details) { + this(message, new EncodedValue(details), null); } - TimeoutType getTimeoutType() { - return timeoutType; + public CanceledFailure(String message) { + this(message, null); } - byte[] getDetails() { + public Value getDetails() { return details; } + + @Override + public void setDataConverter(DataConverter converter) { + ((EncodedValue) details).setDataConverter(converter); + } } diff --git a/src/main/java/io/temporal/failure/ChildWorkflowFailure.java b/src/main/java/io/temporal/failure/ChildWorkflowFailure.java new file mode 100644 index 0000000000..7f09625098 --- /dev/null +++ b/src/main/java/io/temporal/failure/ChildWorkflowFailure.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.failure; + +import io.temporal.proto.common.RetryStatus; +import io.temporal.proto.common.WorkflowExecution; +import java.util.Objects; + +public final class ChildWorkflowFailure extends TemporalFailure { + + private final long initiatedEventId; + private final long startedEventId; + private final String namespace; + private final RetryStatus retryStatus; + private final WorkflowExecution execution; + private final String workflowType; + + public ChildWorkflowFailure( + long initiatedEventId, + long startedEventId, + String workflowType, + WorkflowExecution execution, + String namespace, + RetryStatus retryStatus, + Throwable cause) { + super( + getMessage( + execution, workflowType, initiatedEventId, startedEventId, namespace, retryStatus), + null, + cause); + this.execution = Objects.requireNonNull(execution); + this.workflowType = Objects.requireNonNull(workflowType); + this.initiatedEventId = initiatedEventId; + this.startedEventId = startedEventId; + this.namespace = namespace; + this.retryStatus = retryStatus; + } + + public long getInitiatedEventId() { + return initiatedEventId; + } + + public long getStartedEventId() { + return startedEventId; + } + + public String getNamespace() { + return namespace; + } + + public RetryStatus getRetryStatus() { + return retryStatus; + } + + public WorkflowExecution getExecution() { + return execution; + } + + public String getWorkflowType() { + return workflowType; + } + + public static String getMessage( + WorkflowExecution execution, + String workflowType, + long initiatedEventId, + long startedEventId, + String namespace, + RetryStatus retryStatus) { + return "workflowId='" + + execution.getWorkflowId() + + '\'' + + ", runId='" + + execution.getRunId() + + '\'' + + ", workflowType='" + + workflowType + + '\'' + + ", initiatedEventId=" + + initiatedEventId + + ", startedEventId=" + + startedEventId + + ", namespace='" + + namespace + + '\'' + + ", retryStatus=" + + retryStatus; + } +} diff --git a/src/main/java/io/temporal/failure/FailureConverter.java b/src/main/java/io/temporal/failure/FailureConverter.java new file mode 100644 index 0000000000..a8322f96ba --- /dev/null +++ b/src/main/java/io/temporal/failure/FailureConverter.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.failure; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import io.temporal.client.ActivityCancelledException; +import io.temporal.common.converter.DataConverter; +import io.temporal.common.converter.EncodedValue; +import io.temporal.internal.common.CheckedExceptionWrapper; +import io.temporal.proto.common.ActivityType; +import io.temporal.proto.common.Payloads; +import io.temporal.proto.common.WorkflowType; +import io.temporal.proto.failure.ActivityFailureInfo; +import io.temporal.proto.failure.ApplicationFailureInfo; +import io.temporal.proto.failure.CanceledFailureInfo; +import io.temporal.proto.failure.ChildWorkflowExecutionFailureInfo; +import io.temporal.proto.failure.Failure; +import io.temporal.proto.failure.ResetWorkflowFailureInfo; +import io.temporal.proto.failure.ServerFailureInfo; +import io.temporal.proto.failure.TerminatedFailureInfo; +import io.temporal.proto.failure.TimeoutFailureInfo; +import io.temporal.testing.SimulatedTimeoutFailure; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FailureConverter { + + private static final Logger log = LoggerFactory.getLogger(FailureConverter.class); + + public static final String JAVA_SDK = "JavaSDK"; + /** + * Stop emitting stack trace after this line. Makes serialized stack traces more readable and + * compact as it omits most of framework level code. + */ + private static final ImmutableSet CUTOFF_METHOD_NAMES = + ImmutableSet.of( + "io.temporal.internal.worker.POJOActivityImplementationFactory$POJOActivityImplementation.execute", + "io.temporal.internal.sync.POJODecisionTaskHandler$POJOWorkflowImplementation.execute"); + + /** Used to parse a stack trace line. */ + private static final String TRACE_ELEMENT_REGEXP = + "((?.*)\\.(?.*))\\(((?.*?)(:(?\\d+))?)\\)"; + + private static final Pattern TRACE_ELEMENT_PATTERN = Pattern.compile(TRACE_ELEMENT_REGEXP); + + public static Exception failureToException(Failure failure, DataConverter dataConverter) { + if (failure == null) { + return null; + } + Exception result = failureToExceptionImpl(failure, dataConverter); + if (result instanceof TemporalFailure) { + ((TemporalFailure) result).setFailure(failure); + } + if (failure.getSource().equals(JAVA_SDK) && !failure.getStackTrace().isEmpty()) { + StackTraceElement[] stackTrace = parseStackTrace(failure.getStackTrace()); + result.setStackTrace(stackTrace); + } + return result; + } + + private static Exception failureToExceptionImpl(Failure failure, DataConverter dataConverter) { + Exception cause = + failure.hasCause() ? failureToException(failure.getCause(), dataConverter) : null; + switch (failure.getFailureInfoCase()) { + case APPLICATIONFAILUREINFO: + { + ApplicationFailureInfo info = failure.getApplicationFailureInfo(); + // Unwrap SimulatedTimeoutFailure + if (failure.getSource().equals(JAVA_SDK) + && info.getType().equals(SimulatedTimeoutFailure.class.getName()) + && cause != null) { + return cause; + } + Optional details = + info.hasDetails() ? Optional.of(info.getDetails()) : Optional.empty(); + return new ApplicationFailure( + failure.getMessage(), + info.getType(), + new EncodedValue(details, dataConverter), + info.getNonRetryable(), + cause); + } + case TIMEOUTFAILUREINFO: + { + TimeoutFailureInfo info = failure.getTimeoutFailureInfo(); + Optional lastHeartbeatDetails = + info.hasLastHeartbeatDetails() + ? Optional.of(info.getLastHeartbeatDetails()) + : Optional.empty(); + TimeoutFailure tf = + new TimeoutFailure( + failure.getMessage(), + new EncodedValue(lastHeartbeatDetails, dataConverter), + info.getTimeoutType(), + cause); + tf.setStackTrace(new StackTraceElement[0]); + return tf; + } + case CANCELEDFAILUREINFO: + { + CanceledFailureInfo info = failure.getCanceledFailureInfo(); + Optional details = + info.hasDetails() ? Optional.of(info.getDetails()) : Optional.empty(); + return new CanceledFailure( + failure.getMessage(), new EncodedValue(details, dataConverter), cause); + } + case TERMINATEDFAILUREINFO: + return new TerminatedFailure(failure.getMessage(), cause); + case SERVERFAILUREINFO: + { + ServerFailureInfo info = failure.getServerFailureInfo(); + return new ServerFailure(failure.getMessage(), info.getNonRetryable(), cause); + } + case RESETWORKFLOWFAILUREINFO: + { + ResetWorkflowFailureInfo info = failure.getResetWorkflowFailureInfo(); + Optional details = + info.hasLastHeartbeatDetails() + ? Optional.of(info.getLastHeartbeatDetails()) + : Optional.empty(); + return new ApplicationFailure( + failure.getMessage(), + "ResetWorkflow", + new EncodedValue(details, dataConverter), + false, + cause); + } + case ACTIVITYFAILUREINFO: + { + ActivityFailureInfo info = failure.getActivityFailureInfo(); + return new ActivityFailure( + info.getScheduledEventId(), + info.getStartedEventId(), + info.getActivityType().getName(), + info.getActivityId(), + info.getRetryStatus(), + info.getIdentity(), + cause); + } + case CHILDWORKFLOWEXECUTIONFAILUREINFO: + { + ChildWorkflowExecutionFailureInfo info = failure.getChildWorkflowExecutionFailureInfo(); + return new ChildWorkflowFailure( + info.getInitiatedEventId(), + info.getStartedEventId(), + info.getWorkflowType().getName(), + info.getWorkflowExecution(), + info.getNamespace(), + info.getRetryStatus(), + cause); + } + case FAILUREINFO_NOT_SET: + default: + throw new IllegalArgumentException("Failure info not set"); + } + } + + public static Failure exceptionToFailure(Throwable e) { + e = CheckedExceptionWrapper.unwrap(e); + return exceptionToFailureNoUnwrapping(e); + } + + public static Failure exceptionToFailureNoUnwrapping(Throwable e) { + String message; + if (e instanceof TemporalFailure) { + TemporalFailure tf = (TemporalFailure) e; + if (tf.getFailure().isPresent()) { + return tf.getFailure().get(); + } + message = tf.getOriginalMessage(); + } else { + message = e.getMessage() == null ? "" : e.getMessage(); + } + String stackTrace = serializeStackTrace(e); + Failure.Builder failure = + Failure.newBuilder().setMessage(message).setSource(JAVA_SDK).setStackTrace(stackTrace); + if (e.getCause() != null) { + failure.setCause(exceptionToFailure(e.getCause())); + } + if (e instanceof ApplicationFailure) { + ApplicationFailure ae = (ApplicationFailure) e; + ApplicationFailureInfo.Builder info = + ApplicationFailureInfo.newBuilder() + .setType(ae.getType()) + .setNonRetryable(ae.isNonRetryable()); + Optional details = ((EncodedValue) ae.getDetails()).toPayloads(); + if (details.isPresent()) { + info.setDetails(details.get()); + } + failure.setApplicationFailureInfo(info); + } else if (e instanceof TimeoutFailure) { + TimeoutFailure te = (TimeoutFailure) e; + TimeoutFailureInfo.Builder info = + TimeoutFailureInfo.newBuilder().setTimeoutType(te.getTimeoutType()); + Optional details = ((EncodedValue) te.getLastHeartbeatDetails()).toPayloads(); + if (details.isPresent()) { + info.setLastHeartbeatDetails(details.get()); + } + failure.setTimeoutFailureInfo(info); + } else if (e instanceof CanceledFailure) { + CanceledFailure ce = (CanceledFailure) e; + CanceledFailureInfo.Builder info = CanceledFailureInfo.newBuilder(); + Optional details = ((EncodedValue) ce.getDetails()).toPayloads(); + if (details.isPresent()) { + info.setDetails(details.get()); + } + failure.setCanceledFailureInfo(info); + } else if (e instanceof TerminatedFailure) { + TerminatedFailure te = (TerminatedFailure) e; + failure.setTerminatedFailureInfo(TerminatedFailureInfo.getDefaultInstance()); + } else if (e instanceof ServerFailure) { + ServerFailure se = (ServerFailure) e; + failure.setServerFailureInfo( + ServerFailureInfo.newBuilder().setNonRetryable(se.isNonRetryable())); + } else if (e instanceof ActivityFailure) { + ActivityFailure ae = (ActivityFailure) e; + ActivityFailureInfo.Builder info = + ActivityFailureInfo.newBuilder() + .setActivityId(ae.getActivityId() == null ? "" : ae.getActivityId()) + .setActivityType(ActivityType.newBuilder().setName(ae.getActivityType())) + .setIdentity(ae.getIdentity()) + .setRetryStatus(ae.getRetryStatus()) + .setScheduledEventId(ae.getScheduledEventId()) + .setStartedEventId(ae.getStartedEventId()); + failure.setActivityFailureInfo(info); + } else if (e instanceof ChildWorkflowFailure) { + ChildWorkflowFailure ce = (ChildWorkflowFailure) e; + ChildWorkflowExecutionFailureInfo.Builder info = + ChildWorkflowExecutionFailureInfo.newBuilder() + .setInitiatedEventId(ce.getInitiatedEventId()) + .setStartedEventId(ce.getStartedEventId()) + .setNamespace(ce.getNamespace() == null ? "" : ce.getNamespace()) + .setRetryStatus(ce.getRetryStatus()) + .setWorkflowType(WorkflowType.newBuilder().setName(ce.getWorkflowType())) + .setWorkflowExecution(ce.getExecution()); + failure.setChildWorkflowExecutionFailureInfo(info); + } else if (e instanceof ActivityCancelledException) { + CanceledFailureInfo.Builder info = CanceledFailureInfo.newBuilder(); + failure.setCanceledFailureInfo(info); + } else { + ApplicationFailureInfo.Builder info = + ApplicationFailureInfo.newBuilder() + .setType(e.getClass().getName()) + .setNonRetryable(false); + failure.setApplicationFailureInfo(info); + } + return failure.build(); + } + + /** Parses stack trace serialized using {@link #serializeStackTrace(Throwable)}. */ + public static StackTraceElement[] parseStackTrace(String stackTrace) { + if (Strings.isNullOrEmpty(stackTrace)) { + return new StackTraceElement[0]; + } + try { + @SuppressWarnings("StringSplitter") + String[] lines = stackTrace.split("\r\n|\n"); + StackTraceElement[] result = new StackTraceElement[lines.length]; + for (int i = 0; i < lines.length; i++) { + result[i] = parseStackTraceElement(lines[i]); + } + return result; + } catch (Exception e) { + if (log.isWarnEnabled()) { + log.warn("Failed to parse stack trace: " + stackTrace); + } + return new StackTraceElement[0]; + } + } + + /** + * See {@link StackTraceElement#toString()} for input specification. + * + * @param line line of stack trace. + * @return StackTraceElement that contains data from that line. + */ + private static StackTraceElement parseStackTraceElement(String line) { + Matcher matcher = TRACE_ELEMENT_PATTERN.matcher(line); + if (!matcher.matches()) { + return null; + } + String declaringClass = matcher.group("className"); + String methodName = matcher.group("methodName"); + String fileName = matcher.group("fileName"); + int lineNumber = 0; + String lns = matcher.group("lineNumber"); + if (lns != null && lns.length() > 0) { + try { + lineNumber = Integer.parseInt(matcher.group("lineNumber")); + } catch (NumberFormatException e) { + } + } + return new StackTraceElement(declaringClass, methodName, fileName, lineNumber); + } + + public static String serializeStackTrace(Throwable e) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + StackTraceElement[] trace = e.getStackTrace(); + for (StackTraceElement element : trace) { + pw.println(element); + String fullMethodName = element.getClassName() + "." + element.getMethodName(); + if (CUTOFF_METHOD_NAMES.contains(fullMethodName)) { + break; + } + } + return sw.toString(); + } +} diff --git a/src/main/java/io/temporal/workflow/ChildWorkflowTerminatedException.java b/src/main/java/io/temporal/failure/ServerFailure.java similarity index 57% rename from src/main/java/io/temporal/workflow/ChildWorkflowTerminatedException.java rename to src/main/java/io/temporal/failure/ServerFailure.java index e64de52e82..bf937b38a9 100644 --- a/src/main/java/io/temporal/workflow/ChildWorkflowTerminatedException.java +++ b/src/main/java/io/temporal/failure/ServerFailure.java @@ -17,20 +17,18 @@ * permissions and limitations under the License. */ -package io.temporal.workflow; +package io.temporal.failure; -import io.temporal.proto.common.WorkflowExecution; -import io.temporal.proto.common.WorkflowType; +/** Exceptions originated at the Temporal service. */ +public final class ServerFailure extends TemporalFailure { + private final boolean nonRetryable; -/** - * Indicates that child workflow was forcefully terminated by an external command to Temporal - * service. - */ -@SuppressWarnings("serial") -public final class ChildWorkflowTerminatedException extends ChildWorkflowException { + public ServerFailure(String message, boolean nonRetryable, Throwable cause) { + super(message, message, cause); + this.nonRetryable = nonRetryable; + } - public ChildWorkflowTerminatedException( - long eventId, WorkflowExecution workflowExecution, WorkflowType workflowType) { - super("Terminated", eventId, workflowExecution, workflowType); + public boolean isNonRetryable() { + return nonRetryable; } } diff --git a/src/main/java/io/temporal/workflow/WorkflowOperationException.java b/src/main/java/io/temporal/failure/TemporalException.java similarity index 62% rename from src/main/java/io/temporal/workflow/WorkflowOperationException.java rename to src/main/java/io/temporal/failure/TemporalException.java index 909f504219..e60d9e837b 100644 --- a/src/main/java/io/temporal/workflow/WorkflowOperationException.java +++ b/src/main/java/io/temporal/failure/TemporalException.java @@ -17,23 +17,15 @@ * permissions and limitations under the License. */ -package io.temporal.workflow; +package io.temporal.failure; /** - * Base exception used to communicate a failure that can be thrown by operations requested by a - * workflow code. + * Base class for all exceptions thrown by Temporal SDK. + * + *

Do not extend by the application code. */ -@SuppressWarnings("serial") -public abstract class WorkflowOperationException extends RuntimeException { - - private long eventId; - - protected WorkflowOperationException(String message, long eventId) { - super(message); - this.eventId = eventId; - } - - public long getEventId() { - return eventId; +public class TemporalException extends RuntimeException { + public TemporalException(String message, Throwable cause) { + super(message, cause, false, true); } } diff --git a/src/main/java/io/temporal/failure/TemporalFailure.java b/src/main/java/io/temporal/failure/TemporalFailure.java new file mode 100644 index 0000000000..d8178b59b1 --- /dev/null +++ b/src/main/java/io/temporal/failure/TemporalFailure.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.failure; + +import io.temporal.common.converter.DataConverter; +import io.temporal.proto.failure.Failure; +import java.util.Optional; + +/** + * Represents failures that can cross workflow and activity boundaries. + * + *

Only exceptions that extend this class will be propagated to the caller. + * + *

Never extend this class or any of its derivatives. They are to be used by the SDK code + * only. Throw an instance {@link ApplicationFailure} to pass application specific errors between + * workflows and activities. + * + *

Any unhandled exception thrown by an activity or workflow will be converted to an instance of + * {@link ApplicationFailure}. + */ +public abstract class TemporalFailure extends TemporalException { + private Optional failure = Optional.empty(); + private final String originalMessage; + + protected TemporalFailure(String message, String originalMessage, Throwable cause) { + super(message, cause); + this.originalMessage = originalMessage; + } + + Optional getFailure() { + return failure; + } + + void setFailure(Failure failure) { + this.failure = Optional.of(failure); + } + + public String getOriginalMessage() { + return originalMessage == null ? "" : originalMessage; + } + + public void setDataConverter(DataConverter converter) {} +} diff --git a/src/main/java/io/temporal/failure/TerminatedFailure.java b/src/main/java/io/temporal/failure/TerminatedFailure.java new file mode 100644 index 0000000000..067966a92e --- /dev/null +++ b/src/main/java/io/temporal/failure/TerminatedFailure.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.failure; + +public final class TerminatedFailure extends TemporalFailure { + + public TerminatedFailure(String message, Throwable cause) { + super(message, message, cause); + } +} diff --git a/src/main/java/io/temporal/failure/TimeoutFailure.java b/src/main/java/io/temporal/failure/TimeoutFailure.java new file mode 100644 index 0000000000..d602ef044e --- /dev/null +++ b/src/main/java/io/temporal/failure/TimeoutFailure.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.failure; + +import com.google.common.base.Strings; +import io.temporal.common.converter.DataConverter; +import io.temporal.common.converter.EncodedValue; +import io.temporal.common.converter.Value; +import io.temporal.proto.common.TimeoutType; + +public final class TimeoutFailure extends TemporalFailure { + private final Value lastHeartbeatDetails; + private final TimeoutType timeoutType; + + public TimeoutFailure(String message, Object lastHeartbeatDetails, TimeoutType timeoutType) { + this(message, new EncodedValue(lastHeartbeatDetails), timeoutType, null); + } + + TimeoutFailure( + String message, Value lastHeartbeatDetails, TimeoutType timeoutType, Throwable cause) { + super(getMessage(message, timeoutType), message, cause); + this.lastHeartbeatDetails = lastHeartbeatDetails; + this.timeoutType = timeoutType; + } + + public Value getLastHeartbeatDetails() { + return lastHeartbeatDetails; + } + + public TimeoutType getTimeoutType() { + return timeoutType; + } + + @Override + public void setDataConverter(DataConverter converter) { + ((EncodedValue) lastHeartbeatDetails).setDataConverter(converter); + } + + public static String getMessage(String message, TimeoutType timeoutType) { + return (Strings.isNullOrEmpty(message) ? "" : "message='" + message + "', ") + + "timeoutType=" + + timeoutType; + } +} diff --git a/src/main/java/io/temporal/internal/common/DataConverterUtils.java b/src/main/java/io/temporal/internal/common/DataConverterUtils.java deleted file mode 100644 index 96ad2b257d..0000000000 --- a/src/main/java/io/temporal/internal/common/DataConverterUtils.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. - * - * 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 io.temporal.internal.common; - -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableSet; -import io.temporal.proto.common.Header; -import io.temporal.proto.common.Payload; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.lang.reflect.Field; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class DataConverterUtils { - private static final Logger log = LoggerFactory.getLogger(DataConverterUtils.class); - - /** - * Stop emitting stack trace after this line. Makes serialized stack traces more readable and - * compact as it omits most of framework level code. - */ - private static final ImmutableSet CUTOFF_METHOD_NAMES = - ImmutableSet.of( - "io.temporal.internal.worker.POJOActivityImplementationFactory$POJOActivityImplementation.execute", - "io.temporal.internal.sync.POJODecisionTaskHandler$POJOWorkflowImplementation.execute"); - - /** Used to parse a stack trace line. */ - private static final String TRACE_ELEMENT_REGEXP = - "((?.*)\\.(?.*))\\(((?.*?)(:(?\\d+))?)\\)"; - - private static final Pattern TRACE_ELEMENT_PATTERN = Pattern.compile(TRACE_ELEMENT_REGEXP); - - private static final boolean SETTING_PRIVATE_FIELD_ALLOWED; - - static { - boolean value = false; - try { - Field causeField = Throwable.class.getDeclaredField("cause"); - causeField.setAccessible(true); - causeField.set(new RuntimeException(), null); - value = true; - } catch (IllegalAccessException e) { - } catch (Exception ex) { - throw new Error("Unexpected", ex); - } - SETTING_PRIVATE_FIELD_ALLOWED = value; - } - - /** Are JVM permissions allowing setting private fields using reflection? */ - public static boolean isSettingPrivateFieldAllowed() { - return SETTING_PRIVATE_FIELD_ALLOWED; - } - - public static String serializeStackTrace(Throwable e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - StackTraceElement[] trace = e.getStackTrace(); - for (StackTraceElement element : trace) { - pw.println(element); - String fullMethodName = element.getClassName() + "." + element.getMethodName(); - if (CUTOFF_METHOD_NAMES.contains(fullMethodName)) { - break; - } - } - return sw.toString(); - } - - /** Parses stack trace serialized using {@link #serializeStackTrace(Throwable)}. */ - public static StackTraceElement[] parseStackTrace(String stackTrace) { - if (Strings.isNullOrEmpty(stackTrace)) { - return new StackTraceElement[0]; - } - try { - @SuppressWarnings("StringSplitter") - String[] lines = stackTrace.split("\r\n|\n"); - StackTraceElement[] result = new StackTraceElement[lines.length]; - for (int i = 0; i < lines.length; i++) { - result[i] = parseStackTraceElement(lines[i]); - } - return result; - } catch (Exception e) { - if (log.isWarnEnabled()) { - log.warn("Failed to parse stack trace: " + stackTrace); - } - return new StackTraceElement[0]; - } - } - - /** - * See {@link StackTraceElement#toString()} for input specification. - * - * @param line line of stack trace. - * @return StackTraceElement that contains data from that line. - */ - private static StackTraceElement parseStackTraceElement(String line) { - Matcher matcher = TRACE_ELEMENT_PATTERN.matcher(line); - if (!matcher.matches()) { - return null; - } - String declaringClass = matcher.group("className"); - String methodName = matcher.group("methodName"); - String fileName = matcher.group("fileName"); - int lineNumber = 0; - String lns = matcher.group("lineNumber"); - if (lns != null && lns.length() > 0) { - try { - lineNumber = Integer.parseInt(matcher.group("lineNumber")); - } catch (NumberFormatException e) { - } - } - return new StackTraceElement(declaringClass, methodName, fileName, lineNumber); - } - - /** - * We want to serialize the throwable and its cause separately, so that if the throwable is - * serializable but the cause is not, we can still serialize them correctly (i.e. we serialize the - * throwable correctly and convert the cause to a data converter exception). If existing cause is - * not detached due to security policy then null is returned. - */ - public static Throwable detachCause(Throwable throwable) { - Throwable cause = null; - if (isSettingPrivateFieldAllowed() - && throwable.getCause() != null - && throwable.getCause() != throwable) { - try { - cause = throwable.getCause(); - Field causeField = Throwable.class.getDeclaredField("cause"); - causeField.setAccessible(true); - causeField.set(throwable, null); - } catch (Exception e) { - log.warn("Failed to clear cause in original throwable.", e); - } - } - return cause; - } - - public static Header toHeaderGrpc(Map headers) { - if (headers == null || headers.isEmpty()) { - return null; - } - Header.Builder builder = Header.newBuilder(); - for (Map.Entry item : headers.entrySet()) { - builder.putFields(item.getKey(), item.getValue()); - } - return builder.build(); - } - - private DataConverterUtils() {} -} diff --git a/src/main/java/io/temporal/workflow/ChildWorkflowTimedOutException.java b/src/main/java/io/temporal/internal/common/HeaderUtils.java similarity index 56% rename from src/main/java/io/temporal/workflow/ChildWorkflowTimedOutException.java rename to src/main/java/io/temporal/internal/common/HeaderUtils.java index b41313653d..0625294ab6 100644 --- a/src/main/java/io/temporal/workflow/ChildWorkflowTimedOutException.java +++ b/src/main/java/io/temporal/internal/common/HeaderUtils.java @@ -17,20 +17,24 @@ * permissions and limitations under the License. */ -package io.temporal.workflow; +package io.temporal.internal.common; -import io.temporal.proto.common.WorkflowExecution; -import io.temporal.proto.common.WorkflowType; +import io.temporal.proto.common.Header; +import io.temporal.proto.common.Payload; +import java.util.Map; -/** - * Indicates that a child workflow exceeded its execution timeout and was forcefully terminated by - * the Temporal service. - */ -@SuppressWarnings("serial") -public final class ChildWorkflowTimedOutException extends ChildWorkflowException { +public class HeaderUtils { - public ChildWorkflowTimedOutException( - long eventId, WorkflowExecution workflowExecution, WorkflowType workflowType) { - super("Time Out", eventId, workflowExecution, workflowType); + public static Header toHeaderGrpc(Map headers) { + if (headers == null || headers.isEmpty()) { + return null; + } + Header.Builder builder = Header.newBuilder(); + for (Map.Entry item : headers.entrySet()) { + builder.putFields(item.getKey(), item.getValue()); + } + return builder.build(); } + + private HeaderUtils() {} } diff --git a/src/main/java/io/temporal/internal/common/InternalUtils.java b/src/main/java/io/temporal/internal/common/InternalUtils.java index 3bb3bca3d8..3a2b9f673f 100644 --- a/src/main/java/io/temporal/internal/common/InternalUtils.java +++ b/src/main/java/io/temporal/internal/common/InternalUtils.java @@ -20,7 +20,7 @@ package io.temporal.internal.common; import com.google.common.base.Defaults; -import io.temporal.common.converter.PayloadConverter; +import io.temporal.common.converter.DataConverter; import io.temporal.internal.worker.Shutdownable; import io.temporal.proto.common.Payload; import io.temporal.proto.common.SearchAttributes; @@ -85,12 +85,10 @@ public static Object getValueOrDefault(Object value, Class valueClass) { } public static SearchAttributes convertMapToSearchAttributes( - Map searchAttributes, PayloadConverter converter) { + Map searchAttributes, DataConverter converter) { Map mapOfByteBuffer = new HashMap<>(); searchAttributes.forEach( - (key, value) -> { - mapOfByteBuffer.put(key, converter.toData(value).get()); - }); + (key, value) -> mapOfByteBuffer.put(key, converter.toPayload(value).get())); return SearchAttributes.newBuilder().putAllIndexedFields(mapOfByteBuffer).build(); } diff --git a/src/main/java/io/temporal/internal/common/LocalActivityMarkerData.java b/src/main/java/io/temporal/internal/common/LocalActivityMarkerData.java index 29b415ffc8..d09036dbeb 100644 --- a/src/main/java/io/temporal/internal/common/LocalActivityMarkerData.java +++ b/src/main/java/io/temporal/internal/common/LocalActivityMarkerData.java @@ -19,31 +19,32 @@ package io.temporal.internal.common; -import com.google.common.base.Strings; import io.temporal.common.converter.DataConverter; -import io.temporal.common.converter.PayloadConverter; +import io.temporal.internal.replay.ClockDecisionContext; import io.temporal.proto.common.ActivityType; -import io.temporal.proto.common.Header; -import io.temporal.proto.common.Payload; import io.temporal.proto.common.Payloads; +import io.temporal.proto.event.EventType; +import io.temporal.proto.event.HistoryEvent; import io.temporal.proto.event.MarkerRecordedEventAttributes; +import io.temporal.proto.failure.CanceledFailureInfo; +import io.temporal.proto.failure.Failure; import io.temporal.proto.workflowservice.RespondActivityTaskCanceledRequest; import io.temporal.proto.workflowservice.RespondActivityTaskFailedRequest; import java.time.Duration; import java.util.Optional; public final class LocalActivityMarkerData { - private static final String LOCAL_ACTIVITY_HEADER_KEY = "LocalActivityHeader"; + static final String MARKER_RESULT_KEY = "result"; + static final String MARKER_DATA_KEY = "data"; public static final class Builder { private String activityId; private String activityType; - private String errReason; - private Optional result; + private Optional failure = Optional.empty(); + private Optional result = Optional.empty(); private long replayTimeMillis; private int attempt; private Duration backoff; - private boolean isCancelled; public Builder setActivityId(String activityId) { this.activityId = activityId; @@ -51,30 +52,32 @@ public Builder setActivityId(String activityId) { } public Builder setActivityType(ActivityType activityType) { - this.activityType = activityType.toString(); + this.activityType = activityType.getName(); return this; } public Builder setTaskFailedRequest(RespondActivityTaskFailedRequest request) { - this.errReason = request.getReason(); - this.result = request.hasDetails() ? Optional.of(request.getDetails()) : Optional.empty(); + this.failure = Optional.of(request.getFailure()); return this; } - public Builder setTaskCancelledRequest( - RespondActivityTaskCanceledRequest request, DataConverter converter) { + public Builder setTaskCancelledRequest(RespondActivityTaskCanceledRequest request) { + CanceledFailureInfo.Builder failureInfo = CanceledFailureInfo.newBuilder(); if (request.hasDetails()) { - Payloads details = request.getDetails(); - String message = converter.fromData(Optional.of(details), String.class, String.class); - this.errReason = message; + failureInfo.setDetails(request.getDetails()); } - this.result = request.hasDetails() ? Optional.of(request.getDetails()) : Optional.empty(); - this.isCancelled = true; + this.failure = Optional.of(Failure.newBuilder().setCanceledFailureInfo(failureInfo).build()); + this.result = Optional.empty(); return this; } - public Builder setResult(Optional result) { - this.result = result; + public Builder setResult(Payloads result) { + this.result = Optional.of(result); + return this; + } + + public Builder setFailure(Failure failure) { + this.failure = Optional.of(failure); return this; } @@ -95,81 +98,68 @@ public Builder setBackoff(Duration backoff) { public LocalActivityMarkerData build() { return new LocalActivityMarkerData( - activityId, - activityType, - replayTimeMillis, - result, - errReason, - attempt, - backoff, - isCancelled); + activityId, activityType, replayTimeMillis, result, failure, attempt, backoff); } } - private static class LocalActivityMarkerHeader { - private final String activityId; - private final String activityType; - private final String errReason; - private final long replayTimeMillis; - private final int attempt; - private final Duration backoff; - private final boolean isCancelled; + private static class DataValue { + private String activityId; + private String activityType; + private long replayTimeMillis; + private int attempt; + private long backoffMillis; + + // Needed by Jackson deserializer + DataValue() {} - LocalActivityMarkerHeader( + DataValue( String activityId, String activityType, long replayTimeMillis, - String errReason, int attempt, - Duration backoff, - boolean isCancelled) { + Duration backoff) { this.activityId = activityId; this.activityType = activityType; this.replayTimeMillis = replayTimeMillis; - this.errReason = errReason; this.attempt = attempt; - this.backoff = backoff; - this.isCancelled = isCancelled; + this.backoffMillis = backoff == null ? 0 : backoff.toMillis(); } } - private final LocalActivityMarkerHeader headers; + private final DataValue data; private final Optional result; + private final Optional failure; private LocalActivityMarkerData( String activityId, String activityType, long replayTimeMillis, Optional result, - String errReason, + Optional failure, int attempt, - Duration backoff, - boolean isCancelled) { - this.headers = - new LocalActivityMarkerHeader( - activityId, activityType, replayTimeMillis, errReason, attempt, backoff, isCancelled); + Duration backoff) { + this.data = new DataValue(activityId, activityType, replayTimeMillis, attempt, backoff); this.result = result; + this.failure = failure; } - private LocalActivityMarkerData(LocalActivityMarkerHeader headers, Optional result) { - this.headers = headers; + private LocalActivityMarkerData( + DataValue data, Optional result, Optional failure) { + this.data = data; this.result = result; + this.failure = failure; } public String getActivityId() { - return headers.activityId; + return data.activityId; } public String getActivityType() { - return headers.activityType; - } - - public String getErrReason() { - return headers.errReason; + return data.activityType; } - public Optional getErrJson() { - return Strings.isNullOrEmpty(headers.errReason) ? Optional.empty() : result; + public Optional getFailure() { + return failure; } public Optional getResult() { @@ -177,38 +167,46 @@ public Optional getResult() { } public long getReplayTimeMillis() { - return headers.replayTimeMillis; + return data.replayTimeMillis; } public int getAttempt() { - return headers.attempt; + return data.attempt; } public Duration getBackoff() { - return headers.backoff; + return Duration.ofMillis(data.backoffMillis); } - public boolean getIsCancelled() { - return headers.isCancelled; - } - - public Header getHeader(PayloadConverter converter) { - Optional headerData = converter.toData(headers); - Header.Builder result = Header.newBuilder(); - if (headerData.isPresent()) { - result.putFields(LOCAL_ACTIVITY_HEADER_KEY, headerData.get()); + public HistoryEvent toEvent(DataConverter converter) { + Payloads data = converter.toPayloads(this.data).get(); + MarkerRecordedEventAttributes.Builder attributes = + MarkerRecordedEventAttributes.newBuilder() + .setMarkerName(ClockDecisionContext.LOCAL_ACTIVITY_MARKER_NAME) + .putDetails(MARKER_DATA_KEY, data); + if (result.isPresent()) { + attributes.putDetails(MARKER_RESULT_KEY, result.get()); + } + if (failure.isPresent()) { + attributes.setFailure(failure.get()); } - return result.build(); + return HistoryEvent.newBuilder() + .setEventType(EventType.MarkerRecorded) + .setMarkerRecordedEventAttributes(attributes) + .build(); } public static LocalActivityMarkerData fromEventAttributes( - MarkerRecordedEventAttributes attributes, PayloadConverter converter) { - Payload payload = attributes.getHeader().getFieldsOrThrow(LOCAL_ACTIVITY_HEADER_KEY); - LocalActivityMarkerHeader header = - converter.fromData( - payload, LocalActivityMarkerHeader.class, LocalActivityMarkerHeader.class); - Optional details = - attributes.hasDetails() ? Optional.of(attributes.getDetails()) : Optional.empty(); - return new LocalActivityMarkerData(header, details); + MarkerRecordedEventAttributes attributes, DataConverter converter) { + Payloads data = attributes.getDetailsOrThrow(MARKER_DATA_KEY); + DataValue laHeader = + converter.fromPayloads(Optional.of(data), DataValue.class, DataValue.class); + Optional result = + attributes.containsDetails(MARKER_RESULT_KEY) + ? Optional.of(attributes.getDetailsOrThrow(MARKER_RESULT_KEY)) + : Optional.empty(); + Optional failure = + attributes.hasFailure() ? Optional.of(attributes.getFailure()) : Optional.empty(); + return new LocalActivityMarkerData(laHeader, result, failure); } } diff --git a/src/main/java/io/temporal/internal/common/RetryParameters.java b/src/main/java/io/temporal/internal/common/RetryParameters.java index 252852a4aa..fe75b6bd48 100644 --- a/src/main/java/io/temporal/internal/common/RetryParameters.java +++ b/src/main/java/io/temporal/internal/common/RetryParameters.java @@ -25,6 +25,7 @@ import io.temporal.common.RetryOptions; import io.temporal.proto.common.RetryPolicy; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public final class RetryParameters { @@ -40,16 +41,8 @@ public RetryParameters(RetryOptions retryOptions) { setMaximumAttempts(retryOptions.getMaximumAttempts()); setInitialIntervalInSeconds(roundUpToSeconds(retryOptions.getInitialInterval())); setMaximumIntervalInSeconds(roundUpToSeconds(retryOptions.getMaximumInterval())); - // Use exception type name as the reason - List reasons = new ArrayList<>(); - // Use exception type name as the reason - List> doNotRetry = retryOptions.getDoNotRetry(); - if (doNotRetry != null) { - for (Class r : doNotRetry) { - reasons.add(r.getName()); - } - setNonRetriableErrorTypes(reasons); - } + List types = new ArrayList<>(Arrays.asList(retryOptions.getDoNotRetry())); + setNonRetriableErrorTypes(types); } public RetryParameters() {} diff --git a/src/main/java/io/temporal/internal/common/Retryer.java b/src/main/java/io/temporal/internal/common/Retryer.java index ec08356b7d..3381a4d161 100644 --- a/src/main/java/io/temporal/internal/common/Retryer.java +++ b/src/main/java/io/temporal/internal/common/Retryer.java @@ -22,6 +22,7 @@ import static io.temporal.internal.common.CheckedExceptionWrapper.unwrap; import io.temporal.common.RetryOptions; +import io.temporal.failure.ApplicationFailure; import java.time.Duration; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -103,8 +104,14 @@ public static R retryWithResult( } catch (Exception e) { throttler.failure(); if (options.getDoNotRetry() != null) { - for (Class exceptionToNotRetry : options.getDoNotRetry()) { - if (exceptionToNotRetry.isAssignableFrom(e.getClass())) { + String type; + if (e instanceof ApplicationFailure) { + type = ((ApplicationFailure) e).getType(); + } else { + type = e.getClass().getName(); + } + for (String exceptionToNotRetry : options.getDoNotRetry()) { + if (exceptionToNotRetry.equals(type)) { rethrow(e); } } @@ -221,11 +228,17 @@ private static ValueExceptionPair failOrRetry( if (e instanceof Error) { return new ValueExceptionPair<>(null, e); } - e = unwrap((Exception) e); + e = unwrap(e); long elapsed = System.currentTimeMillis() - startTime; if (options.getDoNotRetry() != null) { - for (Class exceptionToNotRetry : options.getDoNotRetry()) { - if (exceptionToNotRetry.isAssignableFrom(e.getClass())) { + String type; + if (e instanceof ApplicationFailure) { + type = ((ApplicationFailure) e).getType(); + } else { + type = e.getClass().getName(); + } + for (String exceptionToNotRetry : options.getDoNotRetry()) { + if (exceptionToNotRetry.equals(type)) { return new ValueExceptionPair<>(null, e); } } diff --git a/src/main/java/io/temporal/internal/common/StartWorkflowExecutionParameters.java b/src/main/java/io/temporal/internal/common/StartWorkflowExecutionParameters.java index 45f994e371..92022e975e 100644 --- a/src/main/java/io/temporal/internal/common/StartWorkflowExecutionParameters.java +++ b/src/main/java/io/temporal/internal/common/StartWorkflowExecutionParameters.java @@ -348,14 +348,14 @@ public static StartWorkflowExecutionParameters fromWorkflowOptions(WorkflowOptio rp.setInitialIntervalInSeconds(roundUpToSeconds(retryOptions.getInitialInterval())); rp.setMaximumIntervalInSeconds(roundUpToSeconds(retryOptions.getMaximumInterval())); rp.setMaximumAttempts(retryOptions.getMaximumAttempts()); - List reasons = new ArrayList<>(); + List types = new ArrayList<>(); // Use exception type name as the reason - List> doNotRetry = retryOptions.getDoNotRetry(); + String[] doNotRetry = retryOptions.getDoNotRetry(); if (doNotRetry != null) { - for (Class r : doNotRetry) { - reasons.add(r.getName()); + for (String r : doNotRetry) { + types.add(r); } - rp.setNonRetriableErrorTypes(reasons); + rp.setNonRetriableErrorTypes(types); } parameters.setRetryParameters(rp); } diff --git a/src/main/java/io/temporal/internal/common/WorkflowExecutionFailedException.java b/src/main/java/io/temporal/internal/common/WorkflowExecutionFailedException.java index a26e6feed9..dbfdfdde4a 100644 --- a/src/main/java/io/temporal/internal/common/WorkflowExecutionFailedException.java +++ b/src/main/java/io/temporal/internal/common/WorkflowExecutionFailedException.java @@ -19,31 +19,32 @@ package io.temporal.internal.common; -import io.temporal.proto.common.Payloads; -import java.util.Optional; +import io.temporal.proto.common.RetryStatus; +import io.temporal.proto.failure.Failure; /** Framework level exception. Do not throw or catch in the application level code. */ public final class WorkflowExecutionFailedException extends RuntimeException { - private final Optional details; private final long decisionTaskCompletedEventId; + private final Failure failure; + private final RetryStatus retryStatus; WorkflowExecutionFailedException( - String reason, Optional details, long decisionTaskCompletedEventId) { - super(reason); - this.details = details; + Failure failure, long decisionTaskCompletedEventId, RetryStatus retryStatus) { + this.failure = failure; this.decisionTaskCompletedEventId = decisionTaskCompletedEventId; + this.retryStatus = retryStatus; } - public String getReason() { - return getMessage(); - } - - public Optional getDetails() { - return details; + public Failure getFailure() { + return failure; } public long getDecisionTaskCompletedEventId() { return decisionTaskCompletedEventId; } + + public RetryStatus getRetryStatus() { + return retryStatus; + } } diff --git a/src/main/java/io/temporal/internal/common/WorkflowExecutionUtils.java b/src/main/java/io/temporal/internal/common/WorkflowExecutionUtils.java index c30ac75e37..fde1f7fb93 100644 --- a/src/main/java/io/temporal/internal/common/WorkflowExecutionUtils.java +++ b/src/main/java/io/temporal/internal/common/WorkflowExecutionUtils.java @@ -30,10 +30,15 @@ import com.google.protobuf.TextFormat; import io.grpc.Deadline; import io.grpc.Status; -import io.temporal.client.WorkflowTerminatedException; -import io.temporal.client.WorkflowTimedOutException; +import io.temporal.client.WorkflowFailedException; import io.temporal.common.converter.DataConverter; +import io.temporal.common.converter.EncodedValue; +import io.temporal.failure.CanceledFailure; +import io.temporal.failure.TerminatedFailure; +import io.temporal.failure.TimeoutFailure; import io.temporal.proto.common.Payloads; +import io.temporal.proto.common.RetryStatus; +import io.temporal.proto.common.TimeoutType; import io.temporal.proto.common.WorkflowExecution; import io.temporal.proto.decision.Decision; import io.temporal.proto.decision.DecisionType; @@ -64,7 +69,6 @@ import java.util.List; import java.util.Map.Entry; import java.util.Optional; -import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ForkJoinPool; @@ -105,12 +109,8 @@ public class WorkflowExecutionUtils { * * @param workflowType is optional. * @throws TimeoutException if workflow didn't complete within specified timeout - * @throws CancellationException if workflow was cancelled + * @throws CanceledFailure if workflow was cancelled * @throws WorkflowExecutionFailedException if workflow execution failed - * @throws WorkflowTimedOutException if workflow execution exceeded its execution timeout and was - * forcefully terminated by the Temporal server. - * @throws WorkflowTerminatedException if workflow execution was terminated through an external - * terminate command. */ public static Optional getWorkflowExecutionResult( WorkflowServiceStubs service, @@ -120,8 +120,7 @@ public static Optional getWorkflowExecutionResult( long timeout, TimeUnit unit, DataConverter converter) - throws TimeoutException, CancellationException, WorkflowExecutionFailedException, - WorkflowTerminatedException, WorkflowTimedOutException { + throws TimeoutException { // getIntanceCloseEvent waits for workflow completion including new runs. HistoryEvent closeEvent = getInstanceCloseEvent(service, namespace, workflowExecution, timeout, unit); @@ -162,32 +161,32 @@ private static Optional getResultFromCloseEvent( String message = null; WorkflowExecutionCanceledEventAttributes attributes = closeEvent.getWorkflowExecutionCanceledEventAttributes(); - if (attributes.hasDetails()) { - Payloads details = attributes.getDetails(); - message = converter.fromData(Optional.of(details), String.class, String.class); - } - throw new CancellationException(message); + Optional details = + attributes.hasDetails() ? Optional.of(attributes.getDetails()) : Optional.empty(); + throw new CanceledFailure("Workflow canceled", new EncodedValue(details, converter), null); case WorkflowExecutionFailed: WorkflowExecutionFailedEventAttributes failed = closeEvent.getWorkflowExecutionFailedEventAttributes(); throw new WorkflowExecutionFailedException( - failed.getReason(), - failed.hasDetails() ? Optional.of(failed.getDetails()) : Optional.empty(), - failed.getDecisionTaskCompletedEventId()); + failed.getFailure(), failed.getDecisionTaskCompletedEventId(), failed.getRetryStatus()); case WorkflowExecutionTerminated: WorkflowExecutionTerminatedEventAttributes terminated = closeEvent.getWorkflowExecutionTerminatedEventAttributes(); - throw new WorkflowTerminatedException( + throw new WorkflowFailedException( workflowExecution, - workflowType, - terminated.getReason(), - terminated.getIdentity(), - terminated.getDetails().toByteArray()); + workflowType.orElse(null), + 0, + RetryStatus.NonRetryableFailure, + new TerminatedFailure(terminated.getReason(), null)); case WorkflowExecutionTimedOut: WorkflowExecutionTimedOutEventAttributes timedOut = closeEvent.getWorkflowExecutionTimedOutEventAttributes(); - throw new WorkflowTimedOutException( - workflowExecution, workflowType, timedOut.getTimeoutType()); + throw new WorkflowFailedException( + workflowExecution, + workflowType.orElse(null), + 0, + timedOut.getRetryStatus(), + new TimeoutFailure(null, null, TimeoutType.StartToClose)); default: throw new RuntimeException( "Workflow end state is not completed: " + prettyPrintObject(closeEvent)); diff --git a/src/main/java/io/temporal/internal/external/GenericWorkflowClientExternalImpl.java b/src/main/java/io/temporal/internal/external/GenericWorkflowClientExternalImpl.java index 580edc2d52..34ee11ce2b 100644 --- a/src/main/java/io/temporal/internal/external/GenericWorkflowClientExternalImpl.java +++ b/src/main/java/io/temporal/internal/external/GenericWorkflowClientExternalImpl.java @@ -19,7 +19,7 @@ package io.temporal.internal.external; -import static io.temporal.internal.common.DataConverterUtils.toHeaderGrpc; +import static io.temporal.internal.common.HeaderUtils.toHeaderGrpc; import com.google.common.base.Strings; import com.uber.m3.tally.Scope; diff --git a/src/main/java/io/temporal/internal/external/ManualActivityCompletionClient.java b/src/main/java/io/temporal/internal/external/ManualActivityCompletionClient.java index 8d9d12619e..7cb2164117 100644 --- a/src/main/java/io/temporal/internal/external/ManualActivityCompletionClient.java +++ b/src/main/java/io/temporal/internal/external/ManualActivityCompletionClient.java @@ -19,7 +19,7 @@ package io.temporal.internal.external; -import java.util.concurrent.CancellationException; +import io.temporal.failure.CanceledFailure; public interface ManualActivityCompletionClient { @@ -27,7 +27,7 @@ public interface ManualActivityCompletionClient { void fail(Throwable failure); - void recordHeartbeat(Object details) throws CancellationException; + void recordHeartbeat(Object details) throws CanceledFailure; void reportCancellation(Object details); } diff --git a/src/main/java/io/temporal/internal/external/ManualActivityCompletionClientImpl.java b/src/main/java/io/temporal/internal/external/ManualActivityCompletionClientImpl.java index d94459b895..a0110c3694 100644 --- a/src/main/java/io/temporal/internal/external/ManualActivityCompletionClientImpl.java +++ b/src/main/java/io/temporal/internal/external/ManualActivityCompletionClientImpl.java @@ -27,6 +27,8 @@ import io.temporal.client.ActivityCompletionFailureException; import io.temporal.client.ActivityNotExistsException; import io.temporal.common.converter.DataConverter; +import io.temporal.failure.CanceledFailure; +import io.temporal.failure.FailureConverter; import io.temporal.internal.common.GrpcRetryer; import io.temporal.internal.common.OptionsUtils; import io.temporal.internal.metrics.MetricsType; @@ -44,7 +46,6 @@ import io.temporal.proto.workflowservice.RespondActivityTaskFailedRequest; import io.temporal.serviceclient.WorkflowServiceStubs; import java.util.Optional; -import java.util.concurrent.CancellationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -96,7 +97,7 @@ class ManualActivityCompletionClientImpl implements ManualActivityCompletionClie @Override public void complete(Object result) { - Optional convertedResult = dataConverter.toData(result); + Optional convertedResult = dataConverter.toPayloads(result); if (taskToken != null) { RespondActivityTaskCompletedRequest.Builder request = RespondActivityTaskCompletedRequest.newBuilder() @@ -145,16 +146,15 @@ public void complete(Object result) { } @Override - public void fail(Throwable failure) { - if (failure == null) { - throw new IllegalArgumentException("null failure"); + public void fail(Throwable exception) { + if (exception == null) { + throw new IllegalArgumentException("null exception"); } // When converting failures reason is class name, details are serialized exception. if (taskToken != null) { RespondActivityTaskFailedRequest request = RespondActivityTaskFailedRequest.newBuilder() - .setReason(failure.getClass().getName()) - .setDetails(dataConverter.toData(failure).get()) + .setFailure(FailureConverter.exceptionToFailure(exception)) .setTaskToken(ByteString.copyFrom(taskToken)) .build(); try { @@ -176,8 +176,7 @@ public void fail(Throwable failure) { } RespondActivityTaskFailedByIdRequest request = RespondActivityTaskFailedByIdRequest.newBuilder() - .setReason(failure.getClass().getName()) - .setDetails(dataConverter.toData(failure).get()) + .setFailure(FailureConverter.exceptionToFailure(exception)) .setNamespace(namespace) .setWorkflowId(execution.getWorkflowId()) .setRunId(execution.getRunId()) @@ -200,8 +199,8 @@ public void fail(Throwable failure) { } @Override - public void recordHeartbeat(Object details) throws CancellationException { - Optional convertedDetails = dataConverter.toData(details); + public void recordHeartbeat(Object details) throws CanceledFailure { + Optional convertedDetails = dataConverter.toPayloads(details); if (taskToken != null) { RecordActivityTaskHeartbeatRequest.Builder request = RecordActivityTaskHeartbeatRequest.newBuilder() @@ -252,7 +251,7 @@ public void recordHeartbeat(Object details) throws CancellationException { @Override public void reportCancellation(Object details) { - Optional convertedDetails = dataConverter.toData(details); + Optional convertedDetails = dataConverter.toPayloads(details); if (taskToken != null) { RespondActivityTaskCanceledRequest.Builder request = RespondActivityTaskCanceledRequest.newBuilder() diff --git a/src/main/java/io/temporal/internal/replay/ActivityDecisionContext.java b/src/main/java/io/temporal/internal/replay/ActivityDecisionContext.java index 63018a9706..8b2d8a3eea 100644 --- a/src/main/java/io/temporal/internal/replay/ActivityDecisionContext.java +++ b/src/main/java/io/temporal/internal/replay/ActivityDecisionContext.java @@ -19,25 +19,27 @@ package io.temporal.internal.replay; -import static io.temporal.internal.common.DataConverterUtils.toHeaderGrpc; +import static io.temporal.failure.FailureConverter.JAVA_SDK; +import static io.temporal.internal.common.HeaderUtils.toHeaderGrpc; import io.temporal.activity.ActivityCancellationType; +import io.temporal.failure.CanceledFailure; import io.temporal.internal.common.RetryParameters; import io.temporal.proto.common.ActivityType; import io.temporal.proto.common.Header; import io.temporal.proto.common.Payloads; -import io.temporal.proto.common.TimeoutType; import io.temporal.proto.decision.ScheduleActivityTaskDecisionAttributes; import io.temporal.proto.event.ActivityTaskCanceledEventAttributes; import io.temporal.proto.event.ActivityTaskCompletedEventAttributes; import io.temporal.proto.event.ActivityTaskFailedEventAttributes; import io.temporal.proto.event.ActivityTaskTimedOutEventAttributes; import io.temporal.proto.event.HistoryEvent; +import io.temporal.proto.failure.CanceledFailureInfo; +import io.temporal.proto.failure.Failure; import io.temporal.proto.tasklist.TaskList; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.CancellationException; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -72,7 +74,7 @@ public void accept(Exception cause) { } Runnable immediateCancellationCallback = () -> { - OpenRequestInfo, ActivityType> scheduled = + OpenRequestInfo, OpenActivityInfo> scheduled = scheduledActivities.remove(scheduledEventId); if (scheduled == null) { throw new IllegalArgumentException( @@ -80,7 +82,7 @@ public void accept(Exception cause) { "Activity with activityId=%s and scheduledEventId=%d wasn't found", activityId, scheduledEventId)); } - callback.accept(null, new CancellationException("Cancelled by request")); + callback.accept(null, new CanceledFailure("Cancelled by request")); }; if (cancellationType != ActivityCancellationType.WAIT_CANCELLATION_COMPLETED) { immediateCancellationCallback.run(); @@ -94,9 +96,42 @@ public void accept(Exception cause) { private final DecisionsHelper decisions; + private static class OpenActivityInfo { + private final ActivityType activityType; + private final String activityId; + private final long scheduledEventId; + private long startedEventId; + + private OpenActivityInfo(ActivityType activityType, String activityId, long scheduledEventId) { + this.activityType = activityType; + this.activityId = activityId; + this.scheduledEventId = scheduledEventId; + } + + public ActivityType getActivityType() { + return activityType; + } + + public String getActivityId() { + return activityId; + } + + public long getScheduledEventId() { + return scheduledEventId; + } + + public long getStartedEventId() { + return startedEventId; + } + + public void setStartedEventId(long startedEventId) { + this.startedEventId = startedEventId; + } + } + // key is scheduledEventId - private final Map, ActivityType>> scheduledActivities = - new HashMap<>(); + private final Map, OpenActivityInfo>> + scheduledActivities = new HashMap<>(); ActivityDecisionContext(DecisionsHelper decisions) { this.decisions = decisions; @@ -104,8 +139,6 @@ public void accept(Exception cause) { Consumer scheduleActivityTask( ExecuteActivityParameters parameters, BiConsumer, Exception> callback) { - final OpenRequestInfo, ActivityType> context = - new OpenRequestInfo<>(parameters.getActivityType()); final ScheduleActivityTaskDecisionAttributes.Builder attributes = ScheduleActivityTaskDecisionAttributes.newBuilder() .setActivityType(parameters.getActivityType()); @@ -143,6 +176,10 @@ Consumer scheduleActivityTask( } long scheduledEventId = decisions.scheduleActivityTask(attributes.build()); + final OpenRequestInfo, OpenActivityInfo> context = + new OpenRequestInfo<>( + new OpenActivityInfo( + parameters.getActivityType(), parameters.getActivityId(), scheduledEventId)); context.setCompletionHandle(callback); scheduledActivities.put(scheduledEventId, context); return new ActivityDecisionContext.ActivityCancellationHandler( @@ -152,13 +189,19 @@ Consumer scheduleActivityTask( void handleActivityTaskCanceled(HistoryEvent event) { ActivityTaskCanceledEventAttributes attributes = event.getActivityTaskCanceledEventAttributes(); if (decisions.handleActivityTaskCanceled(event)) { - CancellationException e = new CancellationException(); - OpenRequestInfo, ActivityType> scheduled = + Failure failure = + Failure.newBuilder() + .setSource(JAVA_SDK) + .setCanceledFailureInfo( + CanceledFailureInfo.newBuilder().setDetails(attributes.getDetails())) + .build(); + FailureWrapperException e = new FailureWrapperException(failure); + OpenRequestInfo, OpenActivityInfo> scheduled = scheduledActivities.remove(attributes.getScheduledEventId()); if (scheduled != null) { BiConsumer, Exception> completionHandle = scheduled.getCompletionCallback(); - // It is OK to fail with subclass of CancellationException when cancellation requested. + // It is OK to fail with subclass of CanceledException when cancellation requested. // It allows passing information about cancellation (details in this case) to the // surrounding doCatch block completionHandle.accept(Optional.empty(), e); @@ -170,7 +213,7 @@ void handleActivityTaskCompleted(HistoryEvent event) { ActivityTaskCompletedEventAttributes attributes = event.getActivityTaskCompletedEventAttributes(); if (decisions.handleActivityTaskClosed(attributes.getScheduledEventId())) { - OpenRequestInfo, ActivityType> scheduled = + OpenRequestInfo, OpenActivityInfo> scheduled = scheduledActivities.remove(attributes.getScheduledEventId()); if (scheduled != null) { Optional result = @@ -190,15 +233,18 @@ void handleActivityTaskCompleted(HistoryEvent event) { void handleActivityTaskFailed(HistoryEvent event) { ActivityTaskFailedEventAttributes attributes = event.getActivityTaskFailedEventAttributes(); if (decisions.handleActivityTaskClosed(attributes.getScheduledEventId())) { - OpenRequestInfo, ActivityType> scheduled = + OpenRequestInfo, OpenActivityInfo> scheduled = scheduledActivities.remove(attributes.getScheduledEventId()); if (scheduled != null) { - String reason = attributes.getReason(); - Optional details = - attributes.hasDetails() ? Optional.of(attributes.getDetails()) : Optional.empty(); + OpenActivityInfo context = scheduled.getUserContext(); ActivityTaskFailedException failure = new ActivityTaskFailedException( - event.getEventId(), scheduled.getUserContext(), null, reason, details); + event.getEventId(), + attributes.getScheduledEventId(), + attributes.getStartedEventId(), + context.getActivityType(), + context.getActivityId(), + attributes.getFailure()); BiConsumer, Exception> completionHandle = scheduled.getCompletionCallback(); completionHandle.accept(Optional.empty(), failure); @@ -209,18 +255,23 @@ void handleActivityTaskFailed(HistoryEvent event) { void handleActivityTaskTimedOut(HistoryEvent event) { ActivityTaskTimedOutEventAttributes attributes = event.getActivityTaskTimedOutEventAttributes(); if (decisions.handleActivityTaskClosed(attributes.getScheduledEventId())) { - OpenRequestInfo, ActivityType> scheduled = + OpenRequestInfo, OpenActivityInfo> scheduled = scheduledActivities.remove(attributes.getScheduledEventId()); if (scheduled != null) { - TimeoutType timeoutType = attributes.getTimeoutType(); - Optional details = - attributes.hasDetails() ? Optional.of(attributes.getDetails()) : Optional.empty(); - ActivityTaskTimeoutException failure = + Failure failure = attributes.getFailure(); + OpenActivityInfo context = scheduled.getUserContext(); + ActivityTaskTimeoutException timeoutException = new ActivityTaskTimeoutException( - event.getEventId(), scheduled.getUserContext(), null, timeoutType, details); + event.getEventId(), + context.getScheduledEventId(), + context.getStartedEventId(), + context.getActivityType(), + context.getActivityId(), + attributes.getRetryStatus(), + failure); BiConsumer, Exception> completionHandle = scheduled.getCompletionCallback(); - completionHandle.accept(Optional.empty(), failure); + completionHandle.accept(Optional.empty(), timeoutException); } } } diff --git a/src/main/java/io/temporal/internal/replay/ActivityTaskFailedException.java b/src/main/java/io/temporal/internal/replay/ActivityTaskFailedException.java index b4414f776d..73fbb7c015 100644 --- a/src/main/java/io/temporal/internal/replay/ActivityTaskFailedException.java +++ b/src/main/java/io/temporal/internal/replay/ActivityTaskFailedException.java @@ -20,34 +20,42 @@ package io.temporal.internal.replay; import io.temporal.proto.common.ActivityType; -import io.temporal.proto.common.Payloads; -import java.util.Optional; +import io.temporal.proto.failure.Failure; /** * Internal. Do not catch or throw in application level code. Exception used to communicate failure * of remote activity. TODO: Make package level visibility. */ @SuppressWarnings("serial") -public class ActivityTaskFailedException extends RuntimeException { +public class ActivityTaskFailedException extends FailureWrapperException { + private final long scheduledEventId; + private final long startedEventId; private final long eventId; private final ActivityType activityType; private final String activityId; - private final Optional details; - private final String reason; ActivityTaskFailedException( long eventId, + long scheduledEventId, + long startedEventId, ActivityType activityType, String activityId, - String reason, - Optional details) { - super(reason); + Failure failure) { + super(failure); + this.scheduledEventId = scheduledEventId; + this.startedEventId = startedEventId; this.eventId = eventId; this.activityType = activityType; this.activityId = activityId; - this.reason = reason; - this.details = details; + } + + public long getScheduledEventId() { + return scheduledEventId; + } + + public long getStartedEventId() { + return startedEventId; } public long getEventId() { @@ -61,12 +69,4 @@ public ActivityType getActivityType() { public String getActivityId() { return activityId; } - - public Optional getDetails() { - return details; - } - - public String getReason() { - return reason; - } } diff --git a/src/main/java/io/temporal/internal/replay/ActivityTaskTimeoutException.java b/src/main/java/io/temporal/internal/replay/ActivityTaskTimeoutException.java index 042e5926eb..af2353b629 100644 --- a/src/main/java/io/temporal/internal/replay/ActivityTaskTimeoutException.java +++ b/src/main/java/io/temporal/internal/replay/ActivityTaskTimeoutException.java @@ -20,19 +20,23 @@ package io.temporal.internal.replay; import io.temporal.proto.common.ActivityType; -import io.temporal.proto.common.Payloads; -import io.temporal.proto.common.TimeoutType; -import java.util.Optional; +import io.temporal.proto.common.RetryStatus; +import io.temporal.proto.failure.Failure; -/** Exception that indicates Activity time out. */ +/** + * Internal. Do not catch or throw in application level code. Exception that indicates Activity time + * out. + */ @SuppressWarnings("serial") public final class ActivityTaskTimeoutException extends RuntimeException { + private final long scheduledEventId; + private final long startedEventId; private final long eventId; - private final TimeoutType timeoutType; + private final RetryStatus retryStatus; - private final Optional details; + private final Failure failure; private final ActivityType activityType; @@ -40,29 +44,39 @@ public final class ActivityTaskTimeoutException extends RuntimeException { ActivityTaskTimeoutException( long eventId, + long scheduledEventId, + long startedEventId, ActivityType activityType, String activityId, - TimeoutType timeoutType, - Optional details) { - super(String.valueOf(timeoutType)); + RetryStatus retryStatus, + Failure failure) { + this.scheduledEventId = scheduledEventId; + this.startedEventId = startedEventId; this.eventId = eventId; this.activityType = activityType; this.activityId = activityId; - this.timeoutType = timeoutType; - this.details = details; + this.retryStatus = retryStatus; + this.failure = failure; + } + + public Failure getFailure() { + return failure; + } + + public long getScheduledEventId() { + return scheduledEventId; } - /** @return The value from the last activity heartbeat details field. */ - public Optional getDetails() { - return details; + public long getStartedEventId() { + return startedEventId; } public long getEventId() { return eventId; } - public TimeoutType getTimeoutType() { - return timeoutType; + public RetryStatus getRetryStatus() { + return retryStatus; } public ActivityType getActivityType() { diff --git a/src/main/java/io/temporal/internal/replay/ChildWorkflowTaskFailedException.java b/src/main/java/io/temporal/internal/replay/ChildWorkflowTaskFailedException.java index 1f42bedf4e..171d5d59e6 100644 --- a/src/main/java/io/temporal/internal/replay/ChildWorkflowTaskFailedException.java +++ b/src/main/java/io/temporal/internal/replay/ChildWorkflowTaskFailedException.java @@ -19,10 +19,10 @@ package io.temporal.internal.replay; -import io.temporal.proto.common.Payloads; +import io.temporal.proto.common.RetryStatus; import io.temporal.proto.common.WorkflowExecution; import io.temporal.proto.common.WorkflowType; -import java.util.Optional; +import io.temporal.proto.failure.Failure; /** Internal. Do not catch or throw by application level code. */ @SuppressWarnings("serial") @@ -34,19 +34,21 @@ public class ChildWorkflowTaskFailedException extends RuntimeException { private final WorkflowType workflowType; - private final Optional details; + private final RetryStatus retryStatus; + + private final Failure failure; public ChildWorkflowTaskFailedException( long eventId, WorkflowExecution workflowExecution, WorkflowType workflowType, - String reason, - Optional details) { - super(reason); + RetryStatus retryStatus, + Failure failure) { this.eventId = eventId; this.workflowExecution = workflowExecution; this.workflowType = workflowType; - this.details = details; + this.retryStatus = retryStatus; + this.failure = failure; } public long getEventId() { @@ -61,11 +63,11 @@ public WorkflowType getWorkflowType() { return workflowType; } - public Optional getDetails() { - return details; + public Failure getFailure() { + return failure; } - public String getReason() { - return getMessage(); + public RetryStatus getRetryStatus() { + return retryStatus; } } diff --git a/src/main/java/io/temporal/internal/replay/ClockDecisionContext.java b/src/main/java/io/temporal/internal/replay/ClockDecisionContext.java index d196e641dc..c2df654f3d 100644 --- a/src/main/java/io/temporal/internal/replay/ClockDecisionContext.java +++ b/src/main/java/io/temporal/internal/replay/ClockDecisionContext.java @@ -19,12 +19,16 @@ package io.temporal.internal.replay; +import static io.temporal.internal.replay.MarkerHandler.MUTABLE_MARKER_DATA_KEY; + import com.google.common.base.Strings; import io.temporal.common.converter.DataConverter; +import io.temporal.failure.CanceledFailure; import io.temporal.internal.common.LocalActivityMarkerData; import io.temporal.internal.sync.WorkflowInternal; import io.temporal.internal.worker.LocalActivityWorker; import io.temporal.proto.common.ActivityType; +import io.temporal.proto.common.Header; import io.temporal.proto.common.Payloads; import io.temporal.proto.common.SearchAttributes; import io.temporal.proto.decision.StartTimerDecisionAttributes; @@ -32,14 +36,12 @@ import io.temporal.proto.event.MarkerRecordedEventAttributes; import io.temporal.proto.event.TimerCanceledEventAttributes; import io.temporal.proto.event.TimerFiredEventAttributes; -import io.temporal.workflow.ActivityFailureException; import io.temporal.workflow.Functions.Func; import io.temporal.workflow.Functions.Func1; import java.time.Duration; import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.function.BiConsumer; @@ -182,8 +184,10 @@ private void timerCancelled(long startEventId, Exception reason) { return; } BiConsumer context = scheduled.getCompletionCallback(); - CancellationException exception = new CancellationException("Cancelled by request"); - exception.initCause(reason); + CanceledFailure exception = new CanceledFailure("Cancelled by request"); + if (reason != null) { + exception.initCause(reason); + } context.accept(null, exception); } @@ -205,7 +209,11 @@ Optional sideEffect(Func> func) { throw new Error("sideEffect function failed", e); } } - decisions.recordMarker(SIDE_EFFECT_MARKER_NAME, null, result); + Map details = new HashMap<>(); + if (result.isPresent()) { + details.put(MUTABLE_MARKER_DATA_KEY, result.get()); + } + decisions.recordMarker(SIDE_EFFECT_MARKER_NAME, Optional.empty(), details, Optional.empty()); return result; } @@ -230,10 +238,12 @@ void handleMarkerRecorded(HistoryEvent event) { String name = attributes.getMarkerName(); if (SIDE_EFFECT_MARKER_NAME.equals(name)) { Optional details = - attributes.hasDetails() ? Optional.of(attributes.getDetails()) : Optional.empty(); + attributes.containsDetails(MUTABLE_MARKER_DATA_KEY) + ? Optional.of(attributes.getDetailsOrThrow(MUTABLE_MARKER_DATA_KEY)) + : Optional.empty(); sideEffectResults.put(event.getEventId(), details); } else if (LOCAL_ACTIVITY_MARKER_NAME.equals(name)) { - handleLocalActivityMarker(attributes); + handleLocalActivityMarker(event.getEventId(), attributes); } else if (!MUTABLE_SIDE_EFFECT_MARKER_NAME.equals(name) && !VERSION_MARKER_NAME.equals(name)) { if (log.isWarnEnabled()) { log.warn("Unexpected marker: " + event); @@ -241,40 +251,32 @@ void handleMarkerRecorded(HistoryEvent event) { } } - private void handleLocalActivityMarker(MarkerRecordedEventAttributes attributes) { + private void handleLocalActivityMarker(long eventId, MarkerRecordedEventAttributes attributes) { LocalActivityMarkerData marker = - LocalActivityMarkerData.fromEventAttributes( - attributes, dataConverter.getPayloadConverter()); + LocalActivityMarkerData.fromEventAttributes(attributes, dataConverter); if (pendingLaTasks.containsKey(marker.getActivityId())) { - log.debug("Handle LocalActivityMarker for activity " + marker.getActivityId()); - - Optional details = - attributes.hasDetails() ? Optional.of(attributes.getDetails()) : Optional.empty(); - decisions.recordMarker( - LOCAL_ACTIVITY_MARKER_NAME, - marker.getHeader(dataConverter.getPayloadConverter()), - details); + if (log.isDebugEnabled()) { + log.debug("Handle LocalActivityMarker for activity " + marker.getActivityId()); + } + Map details = attributes.getDetailsMap(); + Optional

header = + attributes.hasHeader() ? Optional.of(attributes.getHeader()) : Optional.empty(); + decisions.recordMarker(LOCAL_ACTIVITY_MARKER_NAME, header, details, marker.getFailure()); OpenRequestInfo, ActivityType> scheduled = pendingLaTasks.remove(marker.getActivityId()); unstartedLaTasks.remove(marker.getActivityId()); Exception failure = null; - if (marker.getIsCancelled()) { - failure = new CancellationException(marker.getErrReason()); - } else if (marker.getErrJson().isPresent()) { - Throwable cause = - dataConverter.fromData(marker.getErrJson(), Throwable.class, Throwable.class); - ActivityType activityType = - ActivityType.newBuilder().setName(marker.getActivityType()).build(); + if (marker.getFailure().isPresent()) { failure = - new ActivityFailureException( - attributes.getDecisionTaskCompletedEventId(), - activityType, + new ActivityTaskFailedException( + eventId, + 0, + 0, + ActivityType.newBuilder().setName(marker.getActivityType()).build(), marker.getActivityId(), - cause, - marker.getAttempt(), - marker.getBackoff()); + marker.getFailure().get()); } BiConsumer, Exception> completionHandle = @@ -315,13 +317,13 @@ int getVersion(String changeId, DataConverter converter, int minSupported, int m if (stored.isPresent()) { return Optional.empty(); } - return converter.toData(maxSupported); + return converter.toPayloads(maxSupported); }); if (!result.isPresent()) { return WorkflowInternal.DEFAULT_VERSION; } - int version = converter.fromData(result, Integer.class, Integer.class); + int version = converter.fromPayloads(result, Integer.class, Integer.class); validateVersion(changeId, version, minSupported, maxSupported); return version; } diff --git a/src/main/java/io/temporal/internal/replay/DecisionContext.java b/src/main/java/io/temporal/internal/replay/DecisionContext.java index 7cecdf7301..88bf1c54c3 100644 --- a/src/main/java/io/temporal/internal/replay/DecisionContext.java +++ b/src/main/java/io/temporal/internal/replay/DecisionContext.java @@ -145,7 +145,7 @@ Optional mutableSideEffect( * * @param delaySeconds time-interval after which the Value becomes ready in seconds. * @param callback Callback that is called with null parameter after the specified delay. - * CancellationException is passed as a parameter in case of a cancellation. + * CanceledException is passed as a parameter in case of a cancellation. * @return cancellation handle. Invoke {@link Consumer#accept(Object)} to cancel timer. */ Consumer createTimer(long delaySeconds, Consumer callback); diff --git a/src/main/java/io/temporal/internal/replay/DecisionsHelper.java b/src/main/java/io/temporal/internal/replay/DecisionsHelper.java index 915cafc62d..277eb04b70 100644 --- a/src/main/java/io/temporal/internal/replay/DecisionsHelper.java +++ b/src/main/java/io/temporal/internal/replay/DecisionsHelper.java @@ -52,11 +52,13 @@ import io.temporal.proto.event.EventType; import io.temporal.proto.event.ExternalWorkflowExecutionCancelRequestedEventAttributes; import io.temporal.proto.event.HistoryEvent; +import io.temporal.proto.event.MarkerRecordedEventAttributes; import io.temporal.proto.event.RequestCancelExternalWorkflowExecutionFailedEventAttributes; import io.temporal.proto.event.StartChildWorkflowExecutionFailedEventAttributes; import io.temporal.proto.event.TimerCanceledEventAttributes; import io.temporal.proto.event.TimerFiredEventAttributes; import io.temporal.proto.event.WorkflowExecutionStartedEventAttributes; +import io.temporal.proto.failure.Failure; import io.temporal.proto.tasklist.TaskList; import io.temporal.proto.workflowservice.PollForDecisionTaskResponse; import java.util.ArrayList; @@ -474,15 +476,11 @@ void continueAsNewWorkflowExecution(ContinueAsNewWorkflowExecutionParameters con addDecision(decisionId, new CompleteWorkflowStateMachine(decisionId, decision)); } - void failWorkflowExecution(WorkflowExecutionException failure) { + void failWorkflowExecution(WorkflowExecutionException exception) { addAllMissingVersionMarker(); FailWorkflowExecutionDecisionAttributes.Builder attributes = - FailWorkflowExecutionDecisionAttributes.newBuilder().setReason(failure.getReason()); - Optional details = failure.getDetails(); - if (details.isPresent()) { - attributes.setDetails(details.get()); - } + FailWorkflowExecutionDecisionAttributes.newBuilder().setFailure(exception.getFailure()); Decision decision = Decision.newBuilder() .setFailWorkflowExecutionDecisionAttributes(attributes) @@ -509,16 +507,21 @@ void cancelWorkflowExecution() { addDecision(decisionId, new CompleteWorkflowStateMachine(decisionId, decision)); } - void recordMarker(String markerName, Header header, Optional details) { + void recordMarker( + String markerName, + Optional
header, + Map details, + Optional failure) { // no need to call addAllMissingVersionMarker here as all the callers are already doing it. RecordMarkerDecisionAttributes.Builder marker = RecordMarkerDecisionAttributes.newBuilder().setMarkerName(markerName); - if (details.isPresent()) { - marker.setDetails(details.get()); + marker.putAllDetails(details); + if (header.isPresent()) { + marker.setHeader(header.get()); } - if (header != null) { - marker.setHeader(header); + if (failure.isPresent()) { + marker.setFailure(failure.get()); } Decision decision = Decision.newBuilder() @@ -690,9 +693,10 @@ void addAllMissingVersionMarker(Optional changeId, Optional changeId, Optional getData(); - - static MarkerInterface fromEventAttributes( - MarkerRecordedEventAttributes attributes, DataConverter converter) { - Optional details = - attributes.hasDetails() ? Optional.of(attributes.getDetails()) : Optional.empty(); - if (attributes.hasHeader()) { - Header markerHeader = attributes.getHeader(); - if (markerHeader.containsFields(MUTABLE_MARKER_HEADER_KEY)) { - MarkerData.MarkerHeader header = - converter - .getPayloadConverter() - .fromData( - markerHeader.getFieldsOrThrow(MUTABLE_MARKER_HEADER_KEY), - MarkerData.MarkerHeader.class, - MarkerData.MarkerHeader.class); - - return new MarkerData(header, details); - } - } - return converter.fromData(details, PlainMarkerData.class, PlainMarkerData.class); - } - } - - static final class MarkerData implements MarkerInterface { + static final class MarkerData { private static final class MarkerHeader { - private final String id; - private final long eventId; - private final int accessCount; + private String id; + private long eventId; + private int accessCount; + + // Needed for Jackson deserialization + MarkerHeader() {} MarkerHeader(String id, long eventId, int accessCount) { this.id = id; @@ -110,75 +81,50 @@ private static final class MarkerHeader { private final MarkerHeader header; private final Optional data; + static MarkerData fromEventAttributes( + MarkerRecordedEventAttributes attributes, DataConverter converter) { + Optional details = + attributes.containsDetails(MUTABLE_MARKER_DATA_KEY) + ? Optional.of(attributes.getDetailsOrThrow(MUTABLE_MARKER_DATA_KEY)) + : Optional.empty(); + MarkerData.MarkerHeader header = + converter.fromPayloads( + Optional.of(attributes.getDetailsOrThrow(MUTABLE_MARKER_HEADER_KEY)), + MarkerData.MarkerHeader.class, + MarkerData.MarkerHeader.class); + + return new MarkerData(header, details); + } + MarkerData(String id, long eventId, Optional data, int accessCount) { this.header = new MarkerHeader(id, eventId, accessCount); - this.data = data; + this.data = Objects.requireNonNull(data); } MarkerData(MarkerHeader header, Optional data) { this.header = header; - this.data = data; + this.data = Objects.requireNonNull(data); + } + + public MarkerHeader getHeader() { + return header; } - @Override public String getId() { return header.id; } - @Override public long getEventId() { return header.eventId; } - @Override - public Optional getData() { - return data; - } - - @Override public int getAccessCount() { return header.accessCount; } - Header getHeader(PayloadConverter converter) { - Optional headerData = converter.toData(header); - return Header.newBuilder().putFields(MUTABLE_MARKER_HEADER_KEY, headerData.get()).build(); - } - } - - static final class PlainMarkerData implements MarkerInterface { - - private final String id; - private final long eventId; - private final Optional data; - private final int accessCount; - - PlainMarkerData(String id, long eventId, Optional data, int accessCount) { - this.id = id; - this.eventId = eventId; - this.data = data; - this.accessCount = accessCount; - } - - @Override - public String getId() { - return id; - } - - @Override - public long getEventId() { - return eventId; - } - - @Override public Optional getData() { return data; } - - @Override - public int getAccessCount() { - return accessCount; - } } private final DecisionsHelper decisions; @@ -223,7 +169,7 @@ Optional handle( // TODO(maxim): Verify why this is necessary. if (!stored.isPresent()) { mutableMarkerResults.put( - id, new MarkerResult(converter.toData(WorkflowInternal.DEFAULT_VERSION))); + id, new MarkerResult(converter.toPayloads(WorkflowInternal.DEFAULT_VERSION))); } return stored; @@ -249,7 +195,7 @@ private Optional getMarkerDataFromHistory( return Optional.empty(); } - MarkerInterface markerData = MarkerInterface.fromEventAttributes(attributes, converter); + MarkerData markerData = MarkerData.fromEventAttributes(attributes, converter); // access count is used to not return data from the marker before the recorded number of calls if (!markerId.equals(markerData.getId()) || markerData.getAccessCount() > expectedAcccessCount) { @@ -262,6 +208,11 @@ private void recordMutableMarker( String id, long eventId, Optional data, int accessCount, DataConverter converter) { MarkerData marker = new MarkerData(id, eventId, data, accessCount); mutableMarkerResults.put(id, new MarkerResult(data)); - decisions.recordMarker(markerName, marker.getHeader(converter.getPayloadConverter()), data); + Map details = new HashMap<>(); + if (data.isPresent()) { + details.put(MUTABLE_MARKER_DATA_KEY, data.get()); + } + details.put(MUTABLE_MARKER_HEADER_KEY, converter.toPayloads(marker.getHeader()).get()); + decisions.recordMarker(markerName, Optional.empty(), details, Optional.empty()); } } diff --git a/src/main/java/io/temporal/internal/replay/ReplayDecider.java b/src/main/java/io/temporal/internal/replay/ReplayDecider.java index 2bcd18ab3d..2ff1864fd8 100644 --- a/src/main/java/io/temporal/internal/replay/ReplayDecider.java +++ b/src/main/java/io/temporal/internal/replay/ReplayDecider.java @@ -27,6 +27,7 @@ import com.uber.m3.tally.Stopwatch; import io.grpc.Status; import io.temporal.common.converter.DataConverter; +import io.temporal.failure.CanceledFailure; import io.temporal.internal.common.GrpcRetryer; import io.temporal.internal.common.OptionsUtils; import io.temporal.internal.common.RpcRetryOptions; @@ -61,7 +62,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; @@ -287,14 +287,14 @@ private void eventLoop() { } catch (WorkflowExecutionException e) { failure = e; completed = true; - } catch (CancellationException e) { + } catch (CanceledFailure e) { if (!cancelRequested) { failure = workflow.mapUnexpectedException(e); } completed = true; } catch (Throwable e) { // can cast as Error is caught above. - failure = workflow.mapUnexpectedException((Exception) e); + failure = workflow.mapUnexpectedException(e); completed = true; } } @@ -505,7 +505,7 @@ private boolean decideImpl( WorkflowQueryResult.newBuilder() .setResultType(QueryResultType.Failed) .setErrorMessage(e.getMessage()) - .setAnswer(converter.toData(stackTrace).get()) + .setAnswer(converter.toPayloads(stackTrace).get()) .build()); } } diff --git a/src/main/java/io/temporal/internal/replay/ReplayDecisionTaskHandler.java b/src/main/java/io/temporal/internal/replay/ReplayDecisionTaskHandler.java index c0281a34eb..37e20d2723 100644 --- a/src/main/java/io/temporal/internal/replay/ReplayDecisionTaskHandler.java +++ b/src/main/java/io/temporal/internal/replay/ReplayDecisionTaskHandler.java @@ -22,7 +22,7 @@ import static io.temporal.internal.common.InternalUtils.createStickyTaskList; import static io.temporal.internal.common.OptionsUtils.roundUpToSeconds; -import com.google.common.base.Throwables; +import io.temporal.failure.FailureConverter; import io.temporal.internal.common.WorkflowExecutionUtils; import io.temporal.internal.metrics.MetricsType; import io.temporal.internal.worker.DecisionTaskHandler; @@ -33,6 +33,7 @@ import io.temporal.proto.common.WorkflowType; import io.temporal.proto.decision.StickyExecutionAttributes; import io.temporal.proto.event.HistoryEvent; +import io.temporal.proto.failure.Failure; import io.temporal.proto.query.QueryResultType; import io.temporal.proto.workflowservice.GetWorkflowExecutionHistoryRequest; import io.temporal.proto.workflowservice.GetWorkflowExecutionHistoryResponse; @@ -116,11 +117,11 @@ public DecisionTaskHandler.Result handleDecisionTask(PollForDecisionTaskResponse + ". If see continuously the workflow might be stuck.", e); } - String stackTrace = Throwables.getStackTraceAsString(e); + Failure failure = FailureConverter.exceptionToFailure(e); RespondDecisionTaskFailedRequest failedRequest = RespondDecisionTaskFailedRequest.newBuilder() .setTaskToken(decisionTask.getTaskToken()) - .setDetails(options.getDataConverter().toData(stackTrace).get()) + .setFailure(failure) .build(); return new DecisionTaskHandler.Result(null, failedRequest, null, null, false); } diff --git a/src/main/java/io/temporal/internal/replay/ReplayWorkflow.java b/src/main/java/io/temporal/internal/replay/ReplayWorkflow.java index a4f6c91401..8a2f4a5f27 100644 --- a/src/main/java/io/temporal/internal/replay/ReplayWorkflow.java +++ b/src/main/java/io/temporal/internal/replay/ReplayWorkflow.java @@ -65,7 +65,7 @@ public interface ReplayWorkflow { * @param failure Unexpected failure cause * @return Serialized failure */ - WorkflowExecutionException mapUnexpectedException(Exception failure); + WorkflowExecutionException mapUnexpectedException(Throwable failure); WorkflowExecutionException mapError(Error failure); diff --git a/src/main/java/io/temporal/internal/replay/WorkflowDecisionContext.java b/src/main/java/io/temporal/internal/replay/WorkflowDecisionContext.java index 49815a6ccc..962e152f00 100644 --- a/src/main/java/io/temporal/internal/replay/WorkflowDecisionContext.java +++ b/src/main/java/io/temporal/internal/replay/WorkflowDecisionContext.java @@ -19,15 +19,22 @@ package io.temporal.internal.replay; -import static io.temporal.internal.common.DataConverterUtils.toHeaderGrpc; - +import static io.temporal.internal.common.HeaderUtils.toHeaderGrpc; + +import io.temporal.client.WorkflowExecutionAlreadyStarted; +import io.temporal.common.converter.EncodedValue; +import io.temporal.failure.CanceledFailure; +import io.temporal.failure.ChildWorkflowFailure; +import io.temporal.failure.TerminatedFailure; +import io.temporal.failure.TimeoutFailure; import io.temporal.internal.common.OptionsUtils; import io.temporal.internal.common.RetryParameters; import io.temporal.proto.common.Header; import io.temporal.proto.common.ParentClosePolicy; import io.temporal.proto.common.Payloads; +import io.temporal.proto.common.RetryStatus; +import io.temporal.proto.common.TimeoutType; import io.temporal.proto.common.WorkflowExecution; -import io.temporal.proto.common.WorkflowType; import io.temporal.proto.decision.RequestCancelExternalWorkflowExecutionDecisionAttributes; import io.temporal.proto.decision.SignalExternalWorkflowExecutionDecisionAttributes; import io.temporal.proto.decision.StartChildWorkflowExecutionDecisionAttributes; @@ -42,13 +49,9 @@ import io.temporal.proto.event.HistoryEvent; import io.temporal.proto.event.SignalExternalWorkflowExecutionFailedEventAttributes; import io.temporal.proto.event.StartChildWorkflowExecutionFailedEventAttributes; -import io.temporal.proto.event.WorkflowExecutionFailedCause; import io.temporal.proto.tasklist.TaskList; import io.temporal.workflow.ChildWorkflowCancellationType; -import io.temporal.workflow.ChildWorkflowTerminatedException; -import io.temporal.workflow.ChildWorkflowTimedOutException; import io.temporal.workflow.SignalExternalWorkflowException; -import io.temporal.workflow.StartChildWorkflowFailedException; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @@ -56,7 +59,6 @@ import java.util.Optional; import java.util.Random; import java.util.UUID; -import java.util.concurrent.CancellationException; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -100,7 +102,7 @@ public void accept(Exception cause) { case ABANDON: case TRY_CANCEL: scheduledExternalWorkflows.remove(initiatedEventId); - CancellationException e = new CancellationException(); + CanceledFailure e = new CanceledFailure("Canceled without waiting", null); BiConsumer, Exception> completionCallback = scheduled.getCompletionCallback(); completionCallback.accept(Optional.empty(), e); @@ -267,7 +269,7 @@ void handleChildWorkflowExecutionCancelRequested(HistoryEvent event) { if (scheduled.getCancellationType() == ChildWorkflowCancellationType.WAIT_CANCELLATION_REQUESTED) { scheduledExternalWorkflows.remove(attributes.getInitiatedEventId()); - CancellationException e = new CancellationException(); + CanceledFailure e = new CanceledFailure("Child workflow cancellation requested"); BiConsumer, Exception> completionCallback = scheduled.getCompletionCallback(); completionCallback.accept(Optional.empty(), e); @@ -281,7 +283,9 @@ void handleChildWorkflowExecutionCanceled(HistoryEvent event) { OpenChildWorkflowRequestInfo scheduled = scheduledExternalWorkflows.remove(attributes.getInitiatedEventId()); if (scheduled != null) { - CancellationException e = new CancellationException(); + // TODO(maxim): Add support for passing details without using converter here + CanceledFailure e = + new CanceledFailure("Child canceled", new EncodedValue(attributes.getDetails()), null); BiConsumer, Exception> completionCallback = scheduled.getCompletionCallback(); completionCallback.accept(Optional.empty(), e); @@ -307,11 +311,17 @@ void handleChildWorkflowExecutionTimedOut(HistoryEvent event) { OpenChildWorkflowRequestInfo scheduled = scheduledExternalWorkflows.remove(attributes.getInitiatedEventId()); if (scheduled != null) { + TimeoutFailure timeoutFailure = new TimeoutFailure(null, null, TimeoutType.StartToClose); + timeoutFailure.setStackTrace(new StackTraceElement[0]); RuntimeException failure = - new ChildWorkflowTimedOutException( - event.getEventId(), + new ChildWorkflowFailure( + attributes.getInitiatedEventId(), + attributes.getStartedEventId(), + attributes.getWorkflowType().getName(), attributes.getWorkflowExecution(), - attributes.getWorkflowType()); + attributes.getNamespace(), + attributes.getRetryStatus(), + timeoutFailure); BiConsumer, Exception> completionCallback = scheduled.getCompletionCallback(); completionCallback.accept(Optional.empty(), failure); @@ -328,8 +338,14 @@ void handleChildWorkflowExecutionTerminated(HistoryEvent event) { scheduledExternalWorkflows.remove(attributes.getInitiatedEventId()); if (scheduled != null) { RuntimeException failure = - new ChildWorkflowTerminatedException( - event.getEventId(), execution, attributes.getWorkflowType()); + new ChildWorkflowFailure( + attributes.getInitiatedEventId(), + attributes.getStartedEventId(), + attributes.getWorkflowType().getName(), + attributes.getWorkflowExecution(), + attributes.getNamespace(), + RetryStatus.NonRetryableFailure, + new TerminatedFailure(null, null)); BiConsumer, Exception> completionCallback = scheduled.getCompletionCallback(); completionCallback.accept(Optional.empty(), failure); @@ -344,13 +360,18 @@ void handleStartChildWorkflowExecutionFailed(HistoryEvent event) { OpenChildWorkflowRequestInfo scheduled = scheduledExternalWorkflows.remove(attributes.getInitiatedEventId()); if (scheduled != null) { - WorkflowExecution workflowExecution = - WorkflowExecution.newBuilder().setWorkflowId(attributes.getWorkflowId()).build(); - WorkflowType workflowType = attributes.getWorkflowType(); - WorkflowExecutionFailedCause cause = attributes.getCause(); - RuntimeException failure = - new StartChildWorkflowFailedException( - event.getEventId(), workflowExecution, workflowType, cause); + Exception failure = + new ChildWorkflowTaskFailedException( + event.getEventId(), + WorkflowExecution.newBuilder().setWorkflowId(attributes.getWorkflowId()).build(), + attributes.getWorkflowType(), + RetryStatus.NonRetryableFailure, + null); + failure.initCause( + new WorkflowExecutionAlreadyStarted( + WorkflowExecution.newBuilder().setWorkflowId(attributes.getWorkflowId()).build(), + attributes.getWorkflowType().getName(), + null)); BiConsumer, Exception> completionCallback = scheduled.getCompletionCallback(); completionCallback.accept(Optional.empty(), failure); @@ -365,16 +386,13 @@ void handleChildWorkflowExecutionFailed(HistoryEvent event) { OpenChildWorkflowRequestInfo scheduled = scheduledExternalWorkflows.remove(attributes.getInitiatedEventId()); if (scheduled != null) { - String reason = attributes.getReason(); - Optional details = - attributes.hasDetails() ? Optional.of(attributes.getDetails()) : Optional.empty(); RuntimeException failure = new ChildWorkflowTaskFailedException( event.getEventId(), attributes.getWorkflowExecution(), attributes.getWorkflowType(), - reason, - details); + attributes.getRetryStatus(), + attributes.getFailure()); BiConsumer, Exception> completionCallback = scheduled.getCompletionCallback(); completionCallback.accept(Optional.empty(), failure); @@ -411,9 +429,7 @@ void handleSignalExternalWorkflowExecutionFailed(HistoryEvent event) { .setWorkflowId(attributes.getWorkflowExecution().getWorkflowId()) .setRunId(attributes.getWorkflowExecution().getRunId()) .build(); - RuntimeException failure = - new SignalExternalWorkflowException( - event.getEventId(), signaledExecution, attributes.getCause()); + RuntimeException failure = new SignalExternalWorkflowException(signaledExecution, null); signalContextAndResult.getCompletionCallback().accept(null, failure); } } diff --git a/src/main/java/io/temporal/internal/sync/ActivityExecutionContext.java b/src/main/java/io/temporal/internal/sync/ActivityExecutionContext.java index 2ab5c3b588..82986f327c 100644 --- a/src/main/java/io/temporal/internal/sync/ActivityExecutionContext.java +++ b/src/main/java/io/temporal/internal/sync/ActivityExecutionContext.java @@ -21,10 +21,10 @@ import io.temporal.activity.ActivityTask; import io.temporal.client.ActivityCompletionException; +import io.temporal.failure.CanceledFailure; import io.temporal.serviceclient.WorkflowServiceStubs; import java.lang.reflect.Type; import java.util.Optional; -import java.util.concurrent.CancellationException; /** * Context object passed to an activity implementation. @@ -50,7 +50,7 @@ public interface ActivityExecutionContext { * * @param details In case of activity timeout details are returned as a field of the exception * thrown. - * @throws CancellationException Indicates that activity cancellation was requested by the + * @throws CanceledFailure Indicates that activity cancellation was requested by the * workflow.Should be rethrown from activity implementation to indicate successful * cancellation. */ diff --git a/src/main/java/io/temporal/internal/sync/ActivityExecutionContextImpl.java b/src/main/java/io/temporal/internal/sync/ActivityExecutionContextImpl.java index 9aa9010dad..66e80f37f7 100644 --- a/src/main/java/io/temporal/internal/sync/ActivityExecutionContextImpl.java +++ b/src/main/java/io/temporal/internal/sync/ActivityExecutionContextImpl.java @@ -119,7 +119,7 @@ public Optional getHeartbeatDetails(Class detailsClass, Type detailsTy return result; } Optional details = task.getHeartbeatDetails(); - return Optional.ofNullable(dataConverter.fromData(details, detailsClass, detailsType)); + return Optional.ofNullable(dataConverter.fromPayloads(details, detailsClass, detailsType)); } finally { lock.unlock(); } @@ -174,7 +174,7 @@ private void sendHeartbeatRequest(Object details) { RecordActivityTaskHeartbeatRequest.Builder r = RecordActivityTaskHeartbeatRequest.newBuilder() .setTaskToken(OptionsUtils.toByteString(task.getTaskToken())); - Optional payloads = dataConverter.toData(details); + Optional payloads = dataConverter.toPayloads(details); if (payloads.isPresent()) { r.setDetails(payloads.get()); } diff --git a/src/main/java/io/temporal/internal/sync/ActivityStubBase.java b/src/main/java/io/temporal/internal/sync/ActivityStubBase.java index 5750e905a8..a7eb26c9f6 100644 --- a/src/main/java/io/temporal/internal/sync/ActivityStubBase.java +++ b/src/main/java/io/temporal/internal/sync/ActivityStubBase.java @@ -20,7 +20,7 @@ package io.temporal.internal.sync; import com.google.common.base.Defaults; -import io.temporal.workflow.ActivityException; +import io.temporal.failure.ActivityFailure; import io.temporal.workflow.ActivityStub; import io.temporal.workflow.Promise; import java.lang.reflect.Type; @@ -42,7 +42,7 @@ public T execute(String activityName, Class resultClass, Type resultType, } try { return result.get(); - } catch (ActivityException e) { + } catch (ActivityFailure e) { // Reset stack to the current one. Otherwise it is very confusing to see a stack of // an event handling method. StackTraceElement[] currentStackTrace = Thread.currentThread().getStackTrace(); diff --git a/src/main/java/io/temporal/internal/sync/ChildWorkflowStubImpl.java b/src/main/java/io/temporal/internal/sync/ChildWorkflowStubImpl.java index 3b08360faf..fe31b9e979 100644 --- a/src/main/java/io/temporal/internal/sync/ChildWorkflowStubImpl.java +++ b/src/main/java/io/temporal/internal/sync/ChildWorkflowStubImpl.java @@ -22,8 +22,8 @@ import com.google.common.base.Defaults; import io.temporal.common.interceptors.WorkflowCallsInterceptor; import io.temporal.common.interceptors.WorkflowCallsInterceptor.WorkflowResult; +import io.temporal.failure.TemporalFailure; import io.temporal.proto.common.WorkflowExecution; -import io.temporal.workflow.ChildWorkflowException; import io.temporal.workflow.ChildWorkflowOptions; import io.temporal.workflow.ChildWorkflowStub; import io.temporal.workflow.CompletablePromise; @@ -32,7 +32,6 @@ import io.temporal.workflow.Workflow; import java.lang.reflect.Type; import java.util.Objects; -import java.util.concurrent.CancellationException; class ChildWorkflowStubImpl implements ChildWorkflowStub { @@ -78,7 +77,7 @@ public R execute(Class resultClass, Type resultType, Object... args) { } try { return result.get(); - } catch (ChildWorkflowException | CancellationException e) { + } catch (TemporalFailure e) { // Reset stack to the current one. Otherwise it is very confusing to see a stack of // an event handling method. e.setStackTrace(Thread.currentThread().getStackTrace()); diff --git a/src/main/java/io/temporal/internal/sync/CompletablePromiseImpl.java b/src/main/java/io/temporal/internal/sync/CompletablePromiseImpl.java index cbf018f116..66a11838d5 100644 --- a/src/main/java/io/temporal/internal/sync/CompletablePromiseImpl.java +++ b/src/main/java/io/temporal/internal/sync/CompletablePromiseImpl.java @@ -19,12 +19,12 @@ package io.temporal.internal.sync; +import io.temporal.failure.TemporalFailure; import io.temporal.workflow.CancellationScope; import io.temporal.workflow.CompletablePromise; import io.temporal.workflow.Functions; import io.temporal.workflow.Promise; import io.temporal.workflow.Workflow; -import io.temporal.workflow.WorkflowOperationException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -131,7 +131,7 @@ public V cancellableGetImpl(boolean cancellable, long timeout, TimeUnit unit) private V throwFailure() { // Replace confusing async stack with the current one. - if (failure instanceof WorkflowOperationException) { + if (failure instanceof TemporalFailure) { failure.setStackTrace(Thread.currentThread().getStackTrace()); } throw failure; diff --git a/src/main/java/io/temporal/internal/sync/DeterministicRunnerImpl.java b/src/main/java/io/temporal/internal/sync/DeterministicRunnerImpl.java index e049b51e78..5ce473df90 100644 --- a/src/main/java/io/temporal/internal/sync/DeterministicRunnerImpl.java +++ b/src/main/java/io/temporal/internal/sync/DeterministicRunnerImpl.java @@ -23,7 +23,6 @@ import com.uber.m3.tally.Scope; import io.temporal.common.context.ContextPropagator; import io.temporal.common.converter.DataConverter; -import io.temporal.common.converter.GsonJsonDataConverter; import io.temporal.internal.common.CheckedExceptionWrapper; import io.temporal.internal.context.ContextThreadLocal; import io.temporal.internal.metrics.NoopScope; @@ -201,7 +200,7 @@ public Thread newThread(Runnable r) { private static SyncDecisionContext newDummySyncDecisionContext() { return new SyncDecisionContext( - new DummyDecisionContext(), GsonJsonDataConverter.getInstance(), null, null); + new DummyDecisionContext(), DataConverter.getDefaultInstance(), null, null); } SyncDecisionContext getDecisionContext() { diff --git a/src/main/java/io/temporal/internal/sync/POJOActivityTaskHandler.java b/src/main/java/io/temporal/internal/sync/POJOActivityTaskHandler.java index 181df59df1..e4ed471766 100644 --- a/src/main/java/io/temporal/internal/sync/POJOActivityTaskHandler.java +++ b/src/main/java/io/temporal/internal/sync/POJOActivityTaskHandler.java @@ -24,15 +24,20 @@ import com.uber.m3.tally.Scope; import io.temporal.client.ActivityCancelledException; import io.temporal.common.converter.DataConverter; -import io.temporal.internal.common.CheckedExceptionWrapper; +import io.temporal.failure.FailureConverter; +import io.temporal.failure.TemporalFailure; +import io.temporal.failure.TimeoutFailure; import io.temporal.internal.metrics.MetricsType; +import io.temporal.internal.replay.FailureWrapperException; import io.temporal.internal.worker.ActivityTaskHandler; import io.temporal.proto.common.Payloads; +import io.temporal.proto.failure.CanceledFailureInfo; +import io.temporal.proto.failure.Failure; import io.temporal.proto.workflowservice.PollForActivityTaskResponse; import io.temporal.proto.workflowservice.RespondActivityTaskCompletedRequest; import io.temporal.proto.workflowservice.RespondActivityTaskFailedRequest; import io.temporal.serviceclient.WorkflowServiceStubs; -import io.temporal.testing.SimulatedTimeoutException; +import io.temporal.testing.SimulatedTimeoutFailure; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collections; @@ -40,7 +45,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CancellationException; import java.util.concurrent.ScheduledExecutorService; import java.util.function.BiFunction; import org.slf4j.Logger; @@ -87,36 +91,26 @@ private void addActivityImplementation( } private ActivityTaskHandler.Result mapToActivityFailure( - Throwable failure, Scope metricsScope, boolean isLocalActivity) { + Throwable exception, Scope metricsScope, boolean isLocalActivity) { - if (failure instanceof ActivityCancelledException) { + if (exception instanceof ActivityCancelledException) { if (isLocalActivity) { metricsScope.counter(MetricsType.LOCAL_ACTIVITY_CANCELED_COUNTER).inc(1); } - throw new CancellationException(failure.getMessage()); + String stackTrace = FailureConverter.serializeStackTrace(exception); + throw new FailureWrapperException( + Failure.newBuilder() + .setStackTrace(stackTrace) + .setCanceledFailureInfo(CanceledFailureInfo.newBuilder()) + .build()); } - - // Only expected during unit tests. - if (failure instanceof SimulatedTimeoutException) { - SimulatedTimeoutException timeoutException = (SimulatedTimeoutException) failure; - Object d = timeoutException.getDetails(); - Optional payloads = dataConverter.toData(d); - byte[] details; - if (payloads.isPresent()) { - details = payloads.get().toByteArray(); - } else { - details = new byte[0]; - } - failure = new SimulatedTimeoutExceptionInternal(timeoutException.getTimeoutType(), details); - } - - if (failure instanceof Error) { + if (exception instanceof Error) { if (isLocalActivity) { metricsScope.counter(MetricsType.LOCAL_ACTIVITY_ERROR_COUNTER).inc(1); } else { metricsScope.counter(MetricsType.ACTIVITY_TASK_ERROR_COUNTER).inc(1); } - throw (Error) failure; + throw (Error) exception; } if (isLocalActivity) { @@ -124,16 +118,17 @@ private ActivityTaskHandler.Result mapToActivityFailure( } else { metricsScope.counter(MetricsType.ACTIVITY_EXEC_FAILED_COUNTER).inc(1); } - - failure = CheckedExceptionWrapper.unwrap(failure); - RespondActivityTaskFailedRequest.Builder result = - RespondActivityTaskFailedRequest.newBuilder().setReason(failure.getClass().getName()); - Optional payloads = dataConverter.toData(failure); - if (payloads.isPresent()) { - result.setDetails(payloads.get()); + if (exception instanceof TemporalFailure) { + ((TemporalFailure) exception).setDataConverter(dataConverter); } + if (exception instanceof TimeoutFailure) { + exception = new SimulatedTimeoutFailure((TimeoutFailure) exception); + } + Failure failure = FailureConverter.exceptionToFailure(exception); + RespondActivityTaskFailedRequest.Builder result = + RespondActivityTaskFailedRequest.newBuilder().setFailure(failure); return new ActivityTaskHandler.Result( - null, new Result.TaskFailedResult(result.build(), failure), null, null); + null, new Result.TaskFailedResult(result.build(), exception), null, null); } @Override @@ -202,7 +197,7 @@ public ActivityTaskHandler.Result execute(ActivityTaskImpl task, Scope metricsSc CurrentActivityExecutionContext.set(context); try { Object[] args = - dataConverter.fromDataArray( + dataConverter.arrayFromPayloads( input, method.getParameterTypes(), method.getGenericParameterTypes()); Object result = method.invoke(activity, args); if (context.isDoNotCompleteOnReturn()) { @@ -211,7 +206,7 @@ public ActivityTaskHandler.Result execute(ActivityTaskImpl task, Scope metricsSc RespondActivityTaskCompletedRequest.Builder request = RespondActivityTaskCompletedRequest.newBuilder(); if (method.getReturnType() != Void.TYPE) { - Optional serialized = dataConverter.toData(result); + Optional serialized = dataConverter.toPayloads(result); if (serialized.isPresent()) { request.setResult(serialized.get()); } @@ -244,13 +239,13 @@ public ActivityTaskHandler.Result execute(ActivityTaskImpl task, Scope metricsSc Optional input = task.getInput(); try { Object[] args = - dataConverter.fromDataArray( + dataConverter.arrayFromPayloads( input, method.getParameterTypes(), method.getGenericParameterTypes()); Object result = method.invoke(activity, args); RespondActivityTaskCompletedRequest.Builder request = RespondActivityTaskCompletedRequest.newBuilder(); if (method.getReturnType() != Void.TYPE) { - Optional payloads = dataConverter.toData(result); + Optional payloads = dataConverter.toPayloads(result); if (payloads.isPresent()) { request.setResult(payloads.get()); } diff --git a/src/main/java/io/temporal/internal/sync/POJOWorkflowImplementationFactory.java b/src/main/java/io/temporal/internal/sync/POJOWorkflowImplementationFactory.java index 9ac8013256..86ee12cc20 100644 --- a/src/main/java/io/temporal/internal/sync/POJOWorkflowImplementationFactory.java +++ b/src/main/java/io/temporal/internal/sync/POJOWorkflowImplementationFactory.java @@ -29,7 +29,9 @@ import io.temporal.common.interceptors.WorkflowInterceptor; import io.temporal.common.interceptors.WorkflowInvocationInterceptor; import io.temporal.common.interceptors.WorkflowInvoker; -import io.temporal.internal.common.CheckedExceptionWrapper; +import io.temporal.failure.CanceledFailure; +import io.temporal.failure.FailureConverter; +import io.temporal.failure.TemporalFailure; import io.temporal.internal.metrics.MetricsType; import io.temporal.internal.replay.DeciderCache; import io.temporal.internal.replay.ReplayWorkflow; @@ -37,7 +39,7 @@ import io.temporal.internal.worker.WorkflowExecutionException; import io.temporal.proto.common.Payloads; import io.temporal.proto.common.WorkflowType; -import io.temporal.testing.SimulatedTimeoutException; +import io.temporal.proto.failure.Failure; import io.temporal.worker.WorkflowImplementationOptions; import io.temporal.workflow.Functions; import io.temporal.workflow.Functions.Func; @@ -52,7 +54,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutorService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -239,16 +240,16 @@ public void initialize() { @Override public Optional execute(Optional input) - throws CancellationException, WorkflowExecutionException { + throws CanceledFailure, WorkflowExecutionException { Object[] args = - dataConverter.fromDataArray( + dataConverter.arrayFromPayloads( input, workflowMethod.getParameterTypes(), workflowMethod.getGenericParameterTypes()); Preconditions.checkNotNull(workflowInvoker, "initialize not called"); Object result = workflowInvoker.execute(args); if (workflowMethod.getReturnType() == Void.TYPE) { return Optional.empty(); } - return dataConverter.toData(result); + return dataConverter.toPayloads(result); } private void newInstance() { @@ -290,8 +291,8 @@ public Object execute(Object[] arguments) { } // Cancellation should be delivered as it impacts which decision closes a // workflow. - if (targetException instanceof CancellationException) { - throw (CancellationException) targetException; + if (targetException instanceof CanceledFailure) { + throw (CanceledFailure) targetException; } if (log.isErrorEnabled()) { log.error( @@ -304,8 +305,7 @@ public Object execute(Object[] arguments) { + context.getWorkflowType(), targetException); } - // Cast to Exception is safe as Error is handled above. - throw mapToWorkflowExecutionException((Exception) targetException, dataConverter); + throw mapToWorkflowExecutionException(targetException, dataConverter); } } @@ -352,31 +352,17 @@ void logSerializationException( } static WorkflowExecutionException mapToWorkflowExecutionException( - Exception failure, DataConverter dataConverter) { - failure = CheckedExceptionWrapper.unwrap(failure); - // Only expected during unit tests. - if (failure instanceof SimulatedTimeoutException) { - SimulatedTimeoutException timeoutException = (SimulatedTimeoutException) failure; - // As SimulatedTimeoutExceptionInternal is serialized to Payloads the details - // is stored as byte array which json serializer understands. - Object d = timeoutException.getDetails(); - Optional payloads = dataConverter.toData(d); - byte[] details; - if (payloads.isPresent()) { - details = payloads.get().toByteArray(); - } else { - details = new byte[0]; - } - failure = new SimulatedTimeoutExceptionInternal(timeoutException.getTimeoutType(), details); + Throwable exception, DataConverter dataConverter) { + if (exception instanceof TemporalFailure) { + ((TemporalFailure) exception).setDataConverter(dataConverter); } - - return new WorkflowExecutionException( - failure.getClass().getName(), dataConverter.toData(failure)); + Failure failure = FailureConverter.exceptionToFailure(exception); + return new WorkflowExecutionException(failure); } - static WorkflowExecutionException mapError(Error failure, DataConverter dataConverter) { - return new WorkflowExecutionException( - failure.getClass().getName(), dataConverter.toData(failure)); + static WorkflowExecutionException mapError(Error error) { + Failure failure = FailureConverter.exceptionToFailureNoUnwrapping(error); + return new WorkflowExecutionException(failure); } @Override diff --git a/src/main/java/io/temporal/internal/sync/SyncDecisionContext.java b/src/main/java/io/temporal/internal/sync/SyncDecisionContext.java index 0dc76e8528..fd413bcb2b 100644 --- a/src/main/java/io/temporal/internal/sync/SyncDecisionContext.java +++ b/src/main/java/io/temporal/internal/sync/SyncDecisionContext.java @@ -21,15 +21,20 @@ import static io.temporal.internal.common.OptionsUtils.roundUpToSeconds; -import com.google.protobuf.InvalidProtocolBufferException; import com.uber.m3.tally.Scope; import io.temporal.activity.ActivityOptions; import io.temporal.activity.LocalActivityOptions; +import io.temporal.client.WorkflowException; import io.temporal.common.RetryOptions; import io.temporal.common.context.ContextPropagator; import io.temporal.common.converter.DataConverter; import io.temporal.common.converter.DataConverterException; import io.temporal.common.interceptors.WorkflowCallsInterceptor; +import io.temporal.failure.ActivityFailure; +import io.temporal.failure.CanceledFailure; +import io.temporal.failure.ChildWorkflowFailure; +import io.temporal.failure.FailureConverter; +import io.temporal.failure.TemporalFailure; import io.temporal.internal.common.InternalUtils; import io.temporal.internal.common.RetryParameters; import io.temporal.internal.metrics.MetricsType; @@ -45,17 +50,12 @@ import io.temporal.proto.common.ActivityType; import io.temporal.proto.common.Payload; import io.temporal.proto.common.Payloads; +import io.temporal.proto.common.RetryStatus; import io.temporal.proto.common.SearchAttributes; import io.temporal.proto.common.WorkflowExecution; import io.temporal.proto.common.WorkflowType; -import io.temporal.workflow.ActivityException; -import io.temporal.workflow.ActivityFailureException; -import io.temporal.workflow.ActivityTimeoutException; import io.temporal.workflow.CancellationScope; -import io.temporal.workflow.ChildWorkflowException; -import io.temporal.workflow.ChildWorkflowFailureException; import io.temporal.workflow.ChildWorkflowOptions; -import io.temporal.workflow.ChildWorkflowTimedOutException; import io.temporal.workflow.CompletablePromise; import io.temporal.workflow.ContinueAsNewOptions; import io.temporal.workflow.Functions; @@ -73,7 +73,6 @@ import java.util.Optional; import java.util.Random; import java.util.UUID; -import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiPredicate; @@ -162,12 +161,12 @@ public Promise executeActivity( Type resultType, Object[] args, ActivityOptions options) { - Optional input = converter.toData(args); + Optional input = converter.toPayloads(args); Promise> binaryResult = executeActivityOnce(activityName, options, input); if (returnClass == Void.TYPE) { return binaryResult.thenApply((r) -> null); } - return binaryResult.thenApply((r) -> converter.fromData(r, returnClass, resultType)); + return binaryResult.thenApply((r) -> converter.fromPayloads(r, returnClass, resultType)); } private Promise> executeActivityOnce( @@ -180,7 +179,7 @@ private Promise> executeActivityOnce( .getCancellationRequest() .thenApply( (reason) -> { - cancellationCallback.accept(new CancellationException(reason)); + cancellationCallback.accept(new CanceledFailure(reason)); return null; }); return callback.result; @@ -205,58 +204,38 @@ private RuntimeException mapActivityException(Exception failure) { if (failure == null) { return null; } - if (failure instanceof CancellationException) { - return (CancellationException) failure; + if (failure instanceof TemporalFailure) { + ((TemporalFailure) failure).setDataConverter(getDataConverter()); + } + if (failure instanceof CanceledFailure) { + return (CanceledFailure) failure; } if (failure instanceof ActivityTaskFailedException) { ActivityTaskFailedException taskFailed = (ActivityTaskFailedException) failure; - String causeClassName = taskFailed.getReason(); - Class causeClass; - Exception cause; - try { - @SuppressWarnings("unchecked") // cc is just to have a place to put this annotation - Class cc = (Class) Class.forName(causeClassName); - causeClass = cc; - cause = getDataConverter().fromData(taskFailed.getDetails(), causeClass, causeClass); - } catch (Exception e) { - cause = e; - } - if (cause instanceof SimulatedTimeoutExceptionInternal) { - // This exception is thrown only in unit tests to mock the activity timeouts - SimulatedTimeoutExceptionInternal testTimeout = (SimulatedTimeoutExceptionInternal) cause; - Optional details; - if (testTimeout.getDetails().length == 0) { - details = Optional.empty(); - } else { - try { - details = Optional.of(Payloads.parseFrom(testTimeout.getDetails())); - } catch (InvalidProtocolBufferException e) { - throw new DataConverterException(e); - } - } - return new ActivityTimeoutException( - taskFailed.getEventId(), - taskFailed.getActivityType(), - taskFailed.getActivityId(), - testTimeout.getTimeoutType(), - details, - getDataConverter()); - } - return new ActivityFailureException( - taskFailed.getEventId(), taskFailed.getActivityType(), taskFailed.getActivityId(), cause); + Throwable exception = + FailureConverter.failureToException(taskFailed.getFailure(), getDataConverter()); + return new ActivityFailure( + taskFailed.getScheduledEventId(), + taskFailed.getStartedEventId(), + taskFailed.getActivityType().getName(), + taskFailed.getActivityId(), + RetryStatus.Timeout, + "", + exception); } if (failure instanceof ActivityTaskTimeoutException) { ActivityTaskTimeoutException timedOut = (ActivityTaskTimeoutException) failure; - return new ActivityTimeoutException( - timedOut.getEventId(), - timedOut.getActivityType(), + return new ActivityFailure( + timedOut.getScheduledEventId(), + timedOut.getStartedEventId(), + timedOut.getActivityType().getName(), timedOut.getActivityId(), - timedOut.getTimeoutType(), - timedOut.getDetails(), - getDataConverter()); + timedOut.getRetryStatus(), + "", + FailureConverter.failureToException(timedOut.getFailure(), converter)); } - if (failure instanceof ActivityException) { - return (ActivityException) failure; + if (failure instanceof ActivityFailure) { + return (ActivityFailure) failure; } throw new IllegalArgumentException( "Unexpected exception type: " + failure.getClass().getName(), failure); @@ -292,13 +271,13 @@ private Promise executeLocalActivityOnce( Type returnType, long elapsed, int attempt) { - Optional input = converter.toData(args); + Optional input = converter.toPayloads(args); Promise> binaryResult = executeLocalActivityOnce(name, options, input, elapsed, attempt); if (returnClass == Void.TYPE) { return binaryResult.thenApply((r) -> null); } - return binaryResult.thenApply((r) -> converter.fromData(r, returnClass, returnType)); + return binaryResult.thenApply((r) -> converter.fromPayloads(r, returnClass, returnType)); } private Promise> executeLocalActivityOnce( @@ -316,7 +295,7 @@ private Promise> executeLocalActivityOnce( .getCancellationRequest() .thenApply( (reason) -> { - cancellationCallback.accept(new CancellationException(reason)); + cancellationCallback.accept(new CanceledFailure(reason)); return null; }); return callback.result; @@ -386,11 +365,11 @@ public WorkflowResult executeChildWorkflow( Type returnType, Object[] args, ChildWorkflowOptions options) { - Optional input = converter.toData(args); + Optional input = converter.toPayloads(args); CompletablePromise execution = Workflow.newPromise(); Promise> output = executeChildWorkflow(workflowType, options, input, execution); - Promise result = output.thenApply((b) -> converter.fromData(b, returnClass, returnType)); + Promise result = output.thenApply((b) -> converter.fromPayloads(b, returnClass, returnType)); return new WorkflowResult<>(result, execution); } @@ -401,10 +380,10 @@ private Promise> executeChildWorkflow( CompletablePromise executionResult) { CompletablePromise> result = Workflow.newPromise(); if (CancellationScope.current().isCancelRequested()) { - CancellationException cancellationException = - new CancellationException("execute called from a cancelled scope"); - executionResult.completeExceptionally(cancellationException); - result.completeExceptionally(cancellationException); + CanceledFailure CanceledFailure = + new CanceledFailure("execute called from a cancelled scope"); + executionResult.completeExceptionally(CanceledFailure); + result.completeExceptionally(CanceledFailure); return result; } RetryParameters retryParameters = null; @@ -455,7 +434,7 @@ private Promise> executeChildWorkflow( .getCancellationRequest() .thenApply( (reason) -> { - cancellationCallback.accept(new CancellationException(reason)); + cancellationCallback.accept(new CanceledFailure(reason)); return null; }); return result; @@ -477,35 +456,35 @@ private RuntimeException mapChildWorkflowException(Exception failure) { if (failure == null) { return null; } - if (failure instanceof CancellationException) { - return (CancellationException) failure; + if (failure instanceof TemporalFailure) { + ((TemporalFailure) failure).setDataConverter(getDataConverter()); + } + if (failure instanceof CanceledFailure) { + return (CanceledFailure) failure; + } + if (failure instanceof WorkflowException) { + return (RuntimeException) failure; } - if (failure instanceof ChildWorkflowException) { - return (ChildWorkflowException) failure; + if (failure instanceof ChildWorkflowFailure) { + return (ChildWorkflowFailure) failure; } if (!(failure instanceof ChildWorkflowTaskFailedException)) { return new IllegalArgumentException("Unexpected exception type: ", failure); } ChildWorkflowTaskFailedException taskFailed = (ChildWorkflowTaskFailedException) failure; - String causeClassName = taskFailed.getReason(); - Exception cause; - try { - @SuppressWarnings("unchecked") - Class causeClass = - (Class) Class.forName(causeClassName); - cause = getDataConverter().fromData(taskFailed.getDetails(), causeClass, causeClass); - } catch (Exception e) { - cause = e; - } - if (cause instanceof SimulatedTimeoutExceptionInternal) { - // This exception is thrown only in unit tests to mock the child workflow timeouts - return new ChildWorkflowTimedOutException( - taskFailed.getEventId(), taskFailed.getWorkflowExecution(), taskFailed.getWorkflowType()); - } - return new ChildWorkflowFailureException( - taskFailed.getEventId(), + Throwable cause = + FailureConverter.failureToException(taskFailed.getFailure(), getDataConverter()); + // To support WorkflowExecutionAlreadyStarted set at handleStartChildWorkflowExecutionFailed + if (cause == null) { + cause = failure.getCause(); + } + return new ChildWorkflowFailure( + 0, + 0, + taskFailed.getWorkflowType().getName(), taskFailed.getWorkflowExecution(), - taskFailed.getWorkflowType(), + null, + taskFailed.getRetryStatus(), cause); } @@ -527,7 +506,7 @@ public Promise newTimer(Duration delay) { .thenApply( (reason) -> { timers.removeTimer(fireTime, timer); - timer.completeExceptionally(new CancellationException(reason)); + timer.completeExceptionally(new CanceledFailure(reason)); return null; }); return timer; @@ -540,9 +519,9 @@ public R sideEffect(Class resultClass, Type resultType, Func func) { context.sideEffect( () -> { R r = func.apply(); - return dataConverter.toData(r); + return dataConverter.toPayloads(r); }); - return dataConverter.fromData(result, resultClass, resultType); + return dataConverter.fromPayloads(result, resultClass, resultType); } @Override @@ -556,13 +535,13 @@ public R mutableSideEffect( (storedBinary) -> { Optional stored = storedBinary.map( - (b) -> converter.fromData(Optional.of(b), resultClass, resultType)); + (b) -> converter.fromPayloads(Optional.of(b), resultClass, resultType)); R funcResult = Objects.requireNonNull( func.apply(), "mutableSideEffect function " + "returned null"); if (!stored.isPresent() || updated.test(stored.get(), funcResult)) { unserializedResult.set(funcResult); - return converter.toData(funcResult); + return converter.toPayloads(funcResult); } return Optional.empty(); // returned only when value doesn't need to be updated }); @@ -577,7 +556,7 @@ public R mutableSideEffect( if (unserialized != null) { return unserialized; } - return converter.fromData(payloads, resultClass, resultType); + return converter.fromPayloads(payloads, resultClass, resultType); } @Override @@ -632,9 +611,9 @@ public void registerQuery( queryCallbacks.put( queryType, (input) -> { - Object[] args = converter.fromDataArray(input, argTypes, genericArgTypes); + Object[] args = converter.arrayFromPayloads(input, argTypes, genericArgTypes); Object result = callback.apply(args); - return converter.toData(result); + return converter.toPayloads(result); }); } @@ -650,7 +629,7 @@ public void registerSignal( Functions.Proc2, Long> signalCallback = (input, eventId) -> { try { - Object[] args = converter.fromDataArray(input, argTypes, genericArgTypes); + Object[] args = converter.arrayFromPayloads(input, argTypes, genericArgTypes); callback.apply(args); } catch (DataConverterException e) { logSerializationException(signalType, eventId, e); @@ -706,7 +685,7 @@ public Promise signalExternalWorkflow( parameters.setSignalName(signalName); parameters.setWorkflowId(execution.getWorkflowId()); parameters.setRunId(execution.getRunId()); - Optional input = getDataConverter().toData(args); + Optional input = getDataConverter().toPayloads(args); parameters.setInput(input); CompletablePromise result = Workflow.newPromise(); @@ -727,7 +706,7 @@ public Promise signalExternalWorkflow( .getCancellationRequest() .thenApply( (reason) -> { - cancellationCallback.accept(new CancellationException(reason)); + cancellationCallback.accept(new CanceledFailure(reason)); return null; }); return result; @@ -768,7 +747,7 @@ public void continueAsNew( parameters.setWorkflowTaskTimeoutSeconds(roundUpToSeconds(ops.getWorkflowTaskTimeout())); parameters.setTaskList(ops.getTaskList()); } - parameters.setInput(getDataConverter().toData(args).orElse(null)); + parameters.setInput(getDataConverter().toPayloads(args).orElse(null)); context.continueAsNewOnCompletion(parameters); WorkflowThread.exit(null); } @@ -782,8 +761,8 @@ private RuntimeException mapSignalWorkflowException(Exception failure) { if (failure == null) { return null; } - if (failure instanceof CancellationException) { - return (CancellationException) failure; + if (failure instanceof CanceledFailure) { + return (CanceledFailure) failure; } if (!(failure instanceof SignalExternalWorkflowException)) { @@ -802,7 +781,7 @@ public boolean isLoggingEnabledInReplay() { public R getLastCompletionResult(Class resultClass, Type resultType) { DataConverter dataConverter = getDataConverter(); - return dataConverter.fromData(lastCompletionResult, resultClass, resultType); + return dataConverter.fromPayloads(lastCompletionResult, resultClass, resultType); } @Override @@ -812,8 +791,7 @@ public void upsertSearchAttributes(Map searchAttributes) { } SearchAttributes attr = - InternalUtils.convertMapToSearchAttributes( - searchAttributes, getDataConverter().getPayloadConverter()); + InternalUtils.convertMapToSearchAttributes(searchAttributes, getDataConverter()); context.upsertSearchAttributes(attr); } } diff --git a/src/main/java/io/temporal/internal/sync/SyncWorkflow.java b/src/main/java/io/temporal/internal/sync/SyncWorkflow.java index 47d37b36e0..d89f2910f1 100644 --- a/src/main/java/io/temporal/internal/sync/SyncWorkflow.java +++ b/src/main/java/io/temporal/internal/sync/SyncWorkflow.java @@ -167,7 +167,7 @@ public Optional query(WorkflowQuery query) { return Optional.empty(); } if (WorkflowClient.QUERY_TYPE_STACK_TRACE.equals(query.getQueryType())) { - return dataConverter.toData(runner.stackTrace()); + return dataConverter.toPayloads(runner.stackTrace()); } Optional args = query.hasQueryArgs() ? Optional.of(query.getQueryArgs()) : Optional.empty(); @@ -175,13 +175,13 @@ public Optional query(WorkflowQuery query) { } @Override - public WorkflowExecutionException mapUnexpectedException(Exception failure) { + public WorkflowExecutionException mapUnexpectedException(Throwable failure) { return POJOWorkflowImplementationFactory.mapToWorkflowExecutionException( failure, dataConverter); } @Override public WorkflowExecutionException mapError(Error failure) { - return POJOWorkflowImplementationFactory.mapError(failure, dataConverter); + return POJOWorkflowImplementationFactory.mapError(failure); } } diff --git a/src/main/java/io/temporal/internal/sync/SyncWorkflowWorker.java b/src/main/java/io/temporal/internal/sync/SyncWorkflowWorker.java index 6321f46bf0..7726ac2999 100644 --- a/src/main/java/io/temporal/internal/sync/SyncWorkflowWorker.java +++ b/src/main/java/io/temporal/internal/sync/SyncWorkflowWorker.java @@ -185,10 +185,10 @@ public R queryWorkflowExecution( Type resultType, Object[] args) throws Exception { - Optional serializedArgs = dataConverter.toData(args); + Optional serializedArgs = dataConverter.toPayloads(args); Optional result = workflowWorker.queryWorkflowExecution(execution, queryType, serializedArgs); - return dataConverter.fromData(result, resultClass, resultType); + return dataConverter.fromPayloads(result, resultClass, resultType); } public R queryWorkflowExecution( @@ -198,10 +198,10 @@ public R queryWorkflowExecution( Type resultType, Object[] args) throws Exception { - Optional serializedArgs = dataConverter.toData(args); + Optional serializedArgs = dataConverter.toPayloads(args); Optional result = workflowWorker.queryWorkflowExecution(history, queryType, serializedArgs); - return dataConverter.fromData(result, resultClass, resultType); + return dataConverter.fromPayloads(result, resultClass, resultType); } @Override diff --git a/src/main/java/io/temporal/internal/sync/TestActivityEnvironmentInternal.java b/src/main/java/io/temporal/internal/sync/TestActivityEnvironmentInternal.java index d97f235cff..9119953e4d 100644 --- a/src/main/java/io/temporal/internal/sync/TestActivityEnvironmentInternal.java +++ b/src/main/java/io/temporal/internal/sync/TestActivityEnvironmentInternal.java @@ -32,12 +32,17 @@ import io.temporal.activity.Activity; import io.temporal.activity.ActivityOptions; import io.temporal.activity.LocalActivityOptions; +import io.temporal.common.converter.EncodedValue; import io.temporal.common.interceptors.WorkflowCallsInterceptor; +import io.temporal.failure.ActivityFailure; +import io.temporal.failure.CanceledFailure; +import io.temporal.failure.FailureConverter; import io.temporal.internal.metrics.NoopScope; import io.temporal.internal.worker.ActivityTaskHandler; import io.temporal.internal.worker.ActivityTaskHandler.Result; import io.temporal.proto.common.ActivityType; import io.temporal.proto.common.Payloads; +import io.temporal.proto.common.RetryStatus; import io.temporal.proto.common.WorkflowExecution; import io.temporal.proto.workflowservice.PollForActivityTaskResponse; import io.temporal.proto.workflowservice.RecordActivityTaskHeartbeatRequest; @@ -50,7 +55,6 @@ import io.temporal.serviceclient.WorkflowServiceStubsOptions; import io.temporal.testing.TestActivityEnvironment; import io.temporal.testing.TestEnvironmentOptions; -import io.temporal.workflow.ActivityFailureException; import io.temporal.workflow.ChildWorkflowOptions; import io.temporal.workflow.ContinueAsNewOptions; import io.temporal.workflow.Functions; @@ -68,7 +72,6 @@ import java.util.Optional; import java.util.Random; import java.util.UUID; -import java.util.concurrent.CancellationException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -137,7 +140,7 @@ public void recordActivityTaskHeartbeat( testEnvironmentOptions .getWorkflowClientOptions() .getDataConverter() - .fromData( + .fromPayloads( requestDetails, activityHeartbetListener.valueClass, activityHeartbetListener.valueType); @@ -230,7 +233,7 @@ public Promise executeActivity( Object[] args, ActivityOptions options) { Optional input = - testEnvironmentOptions.getWorkflowClientOptions().getDataConverter().toData(args); + testEnvironmentOptions.getWorkflowClientOptions().getDataConverter().toPayloads(args); PollForActivityTaskResponse.Builder taskBuilder = PollForActivityTaskResponse.newBuilder() .setScheduleToCloseTimeoutSeconds( @@ -373,37 +376,34 @@ private T getReply( return testEnvironmentOptions .getWorkflowClientOptions() .getDataConverter() - .fromData(result, resultClass, resultType); + .fromPayloads(result, resultClass, resultType); } else { RespondActivityTaskFailedRequest taskFailed = response.getTaskFailed().getTaskFailedRequest(); if (taskFailed != null) { - String causeClassName = taskFailed.getReason(); - Class causeClass; - Exception cause; - try { - @SuppressWarnings("unchecked") // cc is just to have a place to put this annotation - Class cc = - (Class) Class.forName(causeClassName); - causeClass = cc; - Optional details = - taskFailed.hasDetails() ? Optional.of(taskFailed.getDetails()) : Optional.empty(); - cause = - testEnvironmentOptions - .getWorkflowClientOptions() - .getDataConverter() - .fromData(details, causeClass, causeClass); - } catch (Exception e) { - cause = e; - } - throw new ActivityFailureException( - 0, task.getActivityType(), task.getActivityId(), cause); - + Exception cause = + FailureConverter.failureToException( + taskFailed.getFailure(), + testEnvironmentOptions.getWorkflowClientOptions().getDataConverter()); + throw new ActivityFailure( + 0, + 0, + task.getActivityType().getName(), + task.getActivityId(), + RetryStatus.NonRetryableFailure, + "TestActivityEnvironment", + cause); } else { RespondActivityTaskCanceledRequest taskCancelled = response.getTaskCancelled(); if (taskCancelled != null) { - throw new CancellationException( - new String(taskCancelled.getDetails().toByteArray(), StandardCharsets.UTF_8)); + throw new CanceledFailure( + "canceled", + new EncodedValue( + taskCancelled.hasDetails() + ? Optional.of(taskCancelled.getDetails()) + : Optional.empty(), + testEnvironmentOptions.getWorkflowClientOptions().getDataConverter()), + null); } } } diff --git a/src/main/java/io/temporal/internal/sync/WorkflowInvocationHandler.java b/src/main/java/io/temporal/internal/sync/WorkflowInvocationHandler.java index 07596ad568..a34bd61ffc 100644 --- a/src/main/java/io/temporal/internal/sync/WorkflowInvocationHandler.java +++ b/src/main/java/io/temporal/internal/sync/WorkflowInvocationHandler.java @@ -20,9 +20,9 @@ package io.temporal.internal.sync; import com.google.common.base.Defaults; -import io.temporal.client.DuplicateWorkflowException; import io.temporal.client.WorkflowClientInterceptor; import io.temporal.client.WorkflowClientOptions; +import io.temporal.client.WorkflowExecutionAlreadyStarted; import io.temporal.client.WorkflowOptions; import io.temporal.client.WorkflowStub; import io.temporal.common.CronSchedule; @@ -183,7 +183,7 @@ private static void startWorkflow(WorkflowStub untyped, Object[] args) { && options.get().getWorkflowIdReusePolicy() == WorkflowIdReusePolicy.AllowDuplicate)) { try { untyped.start(args); - } catch (DuplicateWorkflowException e) { + } catch (WorkflowExecutionAlreadyStarted e) { // We do allow duplicated calls if policy is not AllowDuplicate. Semantic is to wait for // result. if (options.isPresent() diff --git a/src/main/java/io/temporal/internal/sync/WorkflowRetryerInternal.java b/src/main/java/io/temporal/internal/sync/WorkflowRetryerInternal.java index f8065b4e52..16dfe0d275 100644 --- a/src/main/java/io/temporal/internal/sync/WorkflowRetryerInternal.java +++ b/src/main/java/io/temporal/internal/sync/WorkflowRetryerInternal.java @@ -20,7 +20,6 @@ package io.temporal.internal.sync; import io.temporal.common.RetryOptions; -import io.temporal.workflow.ActivityFailureException; import io.temporal.workflow.CompletablePromise; import io.temporal.workflow.Functions; import io.temporal.workflow.Promise; @@ -35,6 +34,54 @@ */ final class WorkflowRetryerInternal { + /** This class is needed as Jackson is not capable to serialize RetryOptions as they are. */ + static class SerializableRetryOptions { + private long initialIntervalMillis; + + private double backoffCoefficient; + + private int maximumAttempts; + + private long maximumIntervalMillis; + + private String[] doNotRetry; + + public SerializableRetryOptions() {} + + public SerializableRetryOptions( + long initialIntervalMillis, + double backoffCoefficient, + int maximumAttempts, + long maximumIntervalMillis, + String[] doNotRetry) { + this.initialIntervalMillis = initialIntervalMillis; + this.backoffCoefficient = backoffCoefficient; + this.maximumAttempts = maximumAttempts; + this.maximumIntervalMillis = maximumIntervalMillis; + this.doNotRetry = doNotRetry; + } + + public long getInitialIntervalMillis() { + return initialIntervalMillis; + } + + public double getBackoffCoefficient() { + return backoffCoefficient; + } + + public int getMaximumAttempts() { + return maximumAttempts; + } + + public long getMaximumIntervalMillis() { + return maximumIntervalMillis; + } + + public String[] getDoNotRetry() { + return doNotRetry; + } + } + /** * Retry procedure synchronously. * @@ -70,13 +117,7 @@ public static R retry( long startTime = WorkflowInternal.currentTimeMillis(); // Records retry options in the history allowing changing them without breaking determinism. String retryId = WorkflowInternal.randomUUID().toString(); - RetryOptions retryOptions = - WorkflowInternal.mutableSideEffect( - retryId, - RetryOptions.class, - RetryOptions.class, - Object::equals, - () -> RetryOptions.newBuilder(options).validateBuildWithDefaults()); + RetryOptions retryOptions = getRetryOptionsSideEffect(retryId, options); while (true) { long nextSleepTime = retryOptions.calculateSleepTime(attempt); try { @@ -114,13 +155,7 @@ private static Promise retryAsync( Functions.Func> func, long startTime, long attempt) { - RetryOptions retryOptions = - WorkflowInternal.mutableSideEffect( - retryId, - RetryOptions.class, - RetryOptions.class, - Object::equals, - () -> RetryOptions.newBuilder(options).validateBuildWithDefaults()); + RetryOptions retryOptions = getRetryOptionsSideEffect(retryId, options); CompletablePromise funcResult = WorkflowInternal.newCompletablePromise(); try { @@ -150,6 +185,39 @@ private static Promise retryAsync( .thenCompose((r) -> r); } + private static RetryOptions getRetryOptionsSideEffect(String retryId, RetryOptions options) { + options = RetryOptions.newBuilder(options).validateBuildWithDefaults(); + SerializableRetryOptions sOptions = + new SerializableRetryOptions( + options.getInitialInterval().toMillis(), + options.getBackoffCoefficient(), + options.getMaximumAttempts(), + options.getMaximumInterval().toMillis(), + options.getDoNotRetry()); + SerializableRetryOptions sRetryOptions = + WorkflowInternal.mutableSideEffect( + retryId, + SerializableRetryOptions.class, + SerializableRetryOptions.class, + Object::equals, + () -> sOptions); + RetryOptions.Builder result = + RetryOptions.newBuilder() + .setBackoffCoefficient(sRetryOptions.getBackoffCoefficient()) + .setInitialInterval(Duration.ofMillis(sRetryOptions.getInitialIntervalMillis())) + .setDoNotRetry(sRetryOptions.getDoNotRetry()); + if (sRetryOptions.getMaximumIntervalMillis() > 0) { + result.setMaximumInterval(Duration.ofMillis(sRetryOptions.getMaximumIntervalMillis())); + } + if (sRetryOptions.getInitialIntervalMillis() > 0) { + result.setInitialInterval(Duration.ofMillis(sRetryOptions.getInitialIntervalMillis())); + } + if (sRetryOptions.getMaximumAttempts() > 0) { + result.setMaximumAttempts(sRetryOptions.getMaximumAttempts()); + } + return result.build(); + } + static Promise retryAsync( Functions.Func2> func, int attempt, long startTime) { @@ -166,22 +234,7 @@ static Promise retryAsync( if (e == null) { return WorkflowInternal.newPromise(r); } - - if (!(e instanceof ActivityFailureException)) { - throw e; - } - - ActivityFailureException afe = (ActivityFailureException) e; - - if (afe.getBackoff() == null) { - throw e; - } - - // newTimer runs in a separate thread, so it performs trampolining eliminating tail - // recursion. - long nextStart = WorkflowInternal.currentTimeMillis() + afe.getBackoff().toMillis(); - return WorkflowInternal.newTimer(afe.getBackoff()) - .thenCompose((nil) -> retryAsync(func, afe.getAttempt() + 1, nextStart)); + throw e; }) .thenCompose((r) -> r); } diff --git a/src/main/java/io/temporal/internal/sync/WorkflowStubImpl.java b/src/main/java/io/temporal/internal/sync/WorkflowStubImpl.java index 82a295f2e4..8c93d26688 100644 --- a/src/main/java/io/temporal/internal/sync/WorkflowStubImpl.java +++ b/src/main/java/io/temporal/internal/sync/WorkflowStubImpl.java @@ -21,10 +21,10 @@ import io.grpc.Status; import io.grpc.StatusRuntimeException; -import io.temporal.client.DuplicateWorkflowException; import io.temporal.client.WorkflowClientOptions; import io.temporal.client.WorkflowException; -import io.temporal.client.WorkflowFailureException; +import io.temporal.client.WorkflowExecutionAlreadyStarted; +import io.temporal.client.WorkflowFailedException; import io.temporal.client.WorkflowNotFoundException; import io.temporal.client.WorkflowOptions; import io.temporal.client.WorkflowQueryException; @@ -34,7 +34,8 @@ import io.temporal.common.context.ContextPropagator; import io.temporal.common.converter.DataConverter; import io.temporal.common.converter.DataConverterException; -import io.temporal.common.converter.GsonJsonDataConverter; +import io.temporal.failure.CanceledFailure; +import io.temporal.failure.FailureConverter; import io.temporal.internal.common.CheckedExceptionWrapper; import io.temporal.internal.common.SignalWithStartWorkflowExecutionParameters; import io.temporal.internal.common.StartWorkflowExecutionParameters; @@ -58,7 +59,6 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.TimeUnit; @@ -105,7 +105,7 @@ class WorkflowStubImpl implements WorkflowStub { public void signal(String signalName, Object... input) { checkStarted(); SignalExternalWorkflowParameters p = new SignalExternalWorkflowParameters(); - p.setInput(clientOptions.getDataConverter().toData(input)); + p.setInput(clientOptions.getDataConverter().toPayloads(input)); p.setSignalName(signalName); p.setWorkflowId(execution.get().getWorkflowId()); // TODO: Deal with signaling started workflow only, when requested @@ -115,12 +115,12 @@ public void signal(String signalName, Object... input) { genericClient.signalWorkflowExecution(p); } catch (StatusRuntimeException e) { if (e.getStatus().getCode() == Status.Code.NOT_FOUND) { - throw new WorkflowNotFoundException(execution.get(), workflowType, e.getMessage()); + throw new WorkflowNotFoundException(execution.get(), workflowType.orElse(null)); } else { - throw new WorkflowServiceException(execution.get(), workflowType, e); + throw new WorkflowServiceException(execution.get(), workflowType.orElse(null), e); } } catch (Exception e) { - throw new WorkflowServiceException(execution.get(), workflowType, e); + throw new WorkflowServiceException(execution.get(), workflowType.orElse(null), e); } } @@ -138,12 +138,12 @@ private WorkflowExecution startWithOptions(WorkflowOptions o, Object... args) { .setRunId(f.getRunId()) .build(); execution.set(exe); - throw new DuplicateWorkflowException(exe, workflowType.get(), e.getMessage()); + throw new WorkflowExecutionAlreadyStarted(exe, workflowType.get(), e); } else { throw e; } } catch (Exception e) { - throw new WorkflowServiceException(execution.get(), workflowType, e); + throw new WorkflowServiceException(execution.get(), workflowType.orElse(null), e); } return execution.get(); } @@ -151,9 +151,7 @@ private WorkflowExecution startWithOptions(WorkflowOptions o, Object... args) { private StartWorkflowExecutionParameters getStartWorkflowExecutionParameters( WorkflowOptions o, Object[] args) { if (execution.get() != null) { - throw new DuplicateWorkflowException( - execution.get(), - workflowType.get(), + throw new IllegalStateException( "Cannot reuse a stub instance to start more than one workflow execution. The stub " + "points to already started execution. If you are trying to wait for a workflow completion either " + "change WorkflowIdReusePolicy from AllowDuplicate or use WorkflowStub.getResult"); @@ -164,7 +162,7 @@ private StartWorkflowExecutionParameters getStartWorkflowExecutionParameters( } else { p.setWorkflowId(o.getWorkflowId()); } - p.setInput(clientOptions.getDataConverter().toData(args)); + p.setInput(clientOptions.getDataConverter().toPayloads(args)); p.setWorkflowType(WorkflowType.newBuilder().setName(workflowType.get()).build()); p.setMemo(convertMemoFromObjectToBytes(o.getMemo())); p.setSearchAttributes(convertSearchAttributesFromObjectToBytes(o.getSearchAttributes())); @@ -180,8 +178,7 @@ private Map convertMapFromObjectToBytes( Map result = new HashMap<>(); for (Map.Entry item : map.entrySet()) { try { - result.put( - item.getKey(), dataConverter.getPayloadConverter().toData(item.getValue()).get()); + result.put(item.getKey(), dataConverter.toPayload(item.getValue()).get()); } catch (DataConverterException e) { throw new DataConverterException("Cannot serialize key " + item.getKey(), e.getCause()); } @@ -194,7 +191,7 @@ private Map convertMemoFromObjectToBytes(Map ma } private Map convertSearchAttributesFromObjectToBytes(Map map) { - return convertMapFromObjectToBytes(map, GsonJsonDataConverter.getInstance()); + return convertMapFromObjectToBytes(map, clientOptions.getDataConverter()); } private Map extractContextsAndConvertToBytes( @@ -221,7 +218,7 @@ private WorkflowExecution signalWithStartWithOptions( WorkflowOptions options, String signalName, Object[] signalArgs, Object[] startArgs) { StartWorkflowExecutionParameters sp = getStartWorkflowExecutionParameters(options, startArgs); - Optional signalInput = clientOptions.getDataConverter().toData(signalArgs); + Optional signalInput = clientOptions.getDataConverter().toPayloads(signalArgs); SignalWithStartWorkflowExecutionParameters p = new SignalWithStartWorkflowExecutionParameters(sp, signalName, signalInput); try { @@ -236,12 +233,12 @@ private WorkflowExecution signalWithStartWithOptions( .setRunId(f.getRunId()) .build(); execution.set(exe); - throw new DuplicateWorkflowException(exe, workflowType.get(), e.getMessage()); + throw new WorkflowExecutionAlreadyStarted(exe, workflowType.get(), e); } else { throw e; } } catch (Exception e) { - throw new WorkflowServiceException(execution.get(), workflowType, e); + throw new WorkflowServiceException(execution.get(), workflowType.orElse(null), e); } return execution.get(); } @@ -301,7 +298,7 @@ public R getResult(long timeout, TimeUnit unit, Class resultClass, Type r timeout, unit, clientOptions.getDataConverter()); - return clientOptions.getDataConverter().fromData(resultValue, resultClass, resultType); + return clientOptions.getDataConverter().fromPayloads(resultValue, resultClass, resultType); } catch (TimeoutException e) { throw e; } catch (Exception e) { @@ -352,7 +349,7 @@ public CompletableFuture getResultAsync( if (r == null) { return null; } - return clientOptions.getDataConverter().fromData(r, resultClass, resultType); + return clientOptions.getDataConverter().fromPayloads(r, resultClass, resultType); }); } @@ -362,38 +359,28 @@ private R mapToWorkflowFailureException( Class detailsClass; if (failure instanceof WorkflowExecutionFailedException) { WorkflowExecutionFailedException executionFailed = (WorkflowExecutionFailedException) failure; - try { - @SuppressWarnings("unchecked") - Class dc = (Class) Class.forName(executionFailed.getReason()); - detailsClass = dc; - } catch (Exception e) { - RuntimeException ee = - new RuntimeException( - "Couldn't deserialize failure cause " - + "as the reason field is expected to contain an exception class name", - executionFailed); - throw new WorkflowFailureException( - execution.get(), workflowType, executionFailed.getDecisionTaskCompletedEventId(), ee); - } Throwable cause = - clientOptions - .getDataConverter() - .fromData(executionFailed.getDetails(), detailsClass, detailsClass); - throw new WorkflowFailureException( - execution.get(), workflowType, executionFailed.getDecisionTaskCompletedEventId(), cause); + FailureConverter.failureToException( + executionFailed.getFailure(), clientOptions.getDataConverter()); + throw new WorkflowFailedException( + execution.get(), + workflowType.orElse(null), + executionFailed.getDecisionTaskCompletedEventId(), + executionFailed.getRetryStatus(), + cause); } else if (failure instanceof StatusRuntimeException) { StatusRuntimeException sre = (StatusRuntimeException) failure; if (sre.getStatus().getCode() == Status.Code.NOT_FOUND) { - throw new WorkflowNotFoundException(execution.get(), workflowType, failure.getMessage()); + throw new WorkflowNotFoundException(execution.get(), workflowType.orElse(null)); } else { - throw new WorkflowServiceException(execution.get(), workflowType, failure); + throw new WorkflowServiceException(execution.get(), workflowType.orElse(null), failure); } - } else if (failure instanceof CancellationException) { - throw (CancellationException) failure; + } else if (failure instanceof CanceledFailure) { + throw (CanceledFailure) failure; } else if (failure instanceof WorkflowException) { throw (WorkflowException) failure; } else { - throw new WorkflowServiceException(execution.get(), workflowType, failure); + throw new WorkflowServiceException(execution.get(), workflowType.orElse(null), failure); } } @@ -406,7 +393,7 @@ public R query(String queryType, Class resultClass, Object... args) { public R query(String queryType, Class resultClass, Type resultType, Object... args) { checkStarted(); QueryWorkflowParameters p = new QueryWorkflowParameters(); - p.setInput(clientOptions.getDataConverter().toData(args)); + p.setInput(clientOptions.getDataConverter().toPayloads(args)); p.setQueryType(queryType); p.setWorkflowId(execution.get().getWorkflowId()); p.setQueryRejectCondition(clientOptions.getQueryRejectCondition()); @@ -417,23 +404,25 @@ public R query(String queryType, Class resultClass, Type resultType, Obje result = genericClient.queryWorkflow(p); } catch (StatusRuntimeException e) { if (e.getStatus().getCode() == Status.Code.NOT_FOUND) { - throw new WorkflowNotFoundException(execution.get(), workflowType, e.getMessage()); + throw new WorkflowNotFoundException(execution.get(), workflowType.orElse(null)); } else if (StatusUtils.hasFailure(e, QueryFailedFailure.class)) { - throw new WorkflowQueryException(execution.get(), e.getMessage()); + throw new WorkflowQueryException(execution.get(), workflowType.orElse(null), e); } - throw new WorkflowServiceException(execution.get(), workflowType, e); + throw new WorkflowServiceException(execution.get(), workflowType.orElse(null), e); } catch (Exception e) { - throw new WorkflowServiceException(execution.get(), workflowType, e); + throw new WorkflowServiceException(execution.get(), workflowType.orElse(null), e); } if (!result.hasQueryRejected()) { Optional queryResult = result.hasQueryResult() ? Optional.of(result.getQueryResult()) : Optional.empty(); - return clientOptions.getDataConverter().fromData(queryResult, resultClass, resultType); + return clientOptions.getDataConverter().fromPayloads(queryResult, resultClass, resultType); } else { throw new WorkflowQueryRejectedException( execution.get(), + workflowType.orElse(null), clientOptions.getQueryRejectCondition(), - result.getQueryRejected().getStatus()); + result.getQueryRejected().getStatus(), + null); } } diff --git a/src/main/java/io/temporal/internal/sync/WorkflowThread.java b/src/main/java/io/temporal/internal/sync/WorkflowThread.java index 1252ef2945..5818bd1698 100644 --- a/src/main/java/io/temporal/internal/sync/WorkflowThread.java +++ b/src/main/java/io/temporal/internal/sync/WorkflowThread.java @@ -21,9 +21,9 @@ import static io.temporal.internal.sync.DeterministicRunnerImpl.currentThreadInternal; +import io.temporal.failure.CanceledFailure; import io.temporal.workflow.CancellationScope; import java.util.Optional; -import java.util.concurrent.CancellationException; import java.util.concurrent.Future; import java.util.function.Supplier; @@ -37,7 +37,7 @@ interface WorkflowThread extends CancellationScope { * @param reason reason for blocking * @param unblockCondition condition that should return true to indicate that thread should * unblock. - * @throws CancellationException if thread (or current cancellation scope was cancelled). + * @throws CanceledFailure if thread (or current cancellation scope was cancelled). * @throws DestroyWorkflowThreadError if thread was asked to be destroyed. */ static void await(String reason, Supplier unblockCondition) diff --git a/src/main/java/io/temporal/internal/sync/WorkflowThreadImpl.java b/src/main/java/io/temporal/internal/sync/WorkflowThreadImpl.java index cb4496c585..6271be9cc3 100644 --- a/src/main/java/io/temporal/internal/sync/WorkflowThreadImpl.java +++ b/src/main/java/io/temporal/internal/sync/WorkflowThreadImpl.java @@ -21,6 +21,7 @@ import com.google.common.util.concurrent.RateLimiter; import io.temporal.common.context.ContextPropagator; +import io.temporal.failure.CanceledFailure; import io.temporal.internal.context.ContextThreadLocal; import io.temporal.internal.logging.LoggerTag; import io.temporal.internal.metrics.MetricsType; @@ -33,7 +34,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -119,7 +119,7 @@ public void run() { String.format("Workflow thread \"%s\" run failed with Error:\n%s", name, stackTrace)); } threadContext.setUnhandledException(e); - } catch (CancellationException e) { + } catch (CanceledFailure e) { if (!isCancelRequested()) { threadContext.setUnhandledException(e); } diff --git a/src/main/java/io/temporal/internal/testservice/RetryState.java b/src/main/java/io/temporal/internal/testservice/RetryState.java index 195aa1e265..63cb29ef34 100644 --- a/src/main/java/io/temporal/internal/testservice/RetryState.java +++ b/src/main/java/io/temporal/internal/testservice/RetryState.java @@ -21,11 +21,36 @@ import io.grpc.Status; import io.temporal.proto.common.RetryPolicy; +import io.temporal.proto.common.RetryStatus; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; final class RetryState { + static class BackoffInterval { + private final int intervalSeconds; + private final RetryStatus retryStatus; + + BackoffInterval(int intervalSeconds) { + this.intervalSeconds = intervalSeconds; + this.retryStatus = RetryStatus.InProgress; + } + + BackoffInterval(RetryStatus retryStatus) { + this.intervalSeconds = -1; + this.retryStatus = retryStatus; + } + + public int getIntervalSeconds() { + return intervalSeconds; + } + + public RetryStatus getRetryStatus() { + return retryStatus; + } + } + private final RetryPolicy retryPolicy; private final long expirationTime; private final int attempt; @@ -56,18 +81,28 @@ RetryState getNextAttempt() { return new RetryState(retryPolicy, expirationTime, attempt + 1); } - int getBackoffIntervalInSeconds(String errReason, long currentTimeMillis) { + BackoffInterval getBackoffIntervalInSeconds(Optional errorType, long currentTimeMillis) { RetryPolicy retryPolicy = getRetryPolicy(); + // check if error is non-retriable + List nonRetryableErrorTypes = retryPolicy.getNonRetryableErrorTypesList(); + if (nonRetryableErrorTypes != null && errorType.isPresent()) { + String type = errorType.get(); + for (String err : nonRetryableErrorTypes) { + if (type.equals(err)) { + return new BackoffInterval(RetryStatus.NonRetryableFailure); + } + } + } long expirationTime = getExpirationTime(); if (retryPolicy.getMaximumAttempts() == 0 && expirationTime == 0) { - return 0; + return new BackoffInterval(RetryStatus.RetryPolicyNotSet); } if (retryPolicy.getMaximumAttempts() > 0 && getAttempt() >= retryPolicy.getMaximumAttempts() - 1) { // currAttempt starts from 0. // MaximumAttempts is the total attempts, including initial (non-retry) attempt. - return 0; + return new BackoffInterval(RetryStatus.MaximumAttemptsReached); } long initInterval = TimeUnit.SECONDS.toMillis(retryPolicy.getInitialIntervalInSeconds()); long nextInterval = @@ -78,7 +113,7 @@ && getAttempt() >= retryPolicy.getMaximumAttempts() - 1) { if (maxInterval > 0) { nextInterval = maxInterval; } else { - return 0; + return new BackoffInterval(RetryStatus.Timeout); } } @@ -90,19 +125,10 @@ && getAttempt() >= retryPolicy.getMaximumAttempts() - 1) { long backoffInterval = nextInterval; long nextScheduleTime = currentTimeMillis + backoffInterval; if (expirationTime != 0 && nextScheduleTime > expirationTime) { - return 0; - } - - // check if error is non-retriable - List nonRetriableErrorReasons = retryPolicy.getNonRetryableErrorTypesList(); - if (nonRetriableErrorReasons != null) { - for (String err : nonRetriableErrorReasons) { - if (errReason.equals(err)) { - return 0; - } - } + return new BackoffInterval(RetryStatus.Timeout); } - return (int) TimeUnit.MILLISECONDS.toSeconds((long) Math.ceil((double) backoffInterval)); + int result = (int) TimeUnit.MILLISECONDS.toSeconds((long) Math.ceil((double) backoffInterval)); + return new BackoffInterval(result); } static RetryPolicy valiateAndOverrideRetryPolicy(RetryPolicy p) { diff --git a/src/main/java/io/temporal/internal/testservice/SelfAdvancingTimerImpl.java b/src/main/java/io/temporal/internal/testservice/SelfAdvancingTimerImpl.java index 3a3ba3f798..76a3bc808c 100644 --- a/src/main/java/io/temporal/internal/testservice/SelfAdvancingTimerImpl.java +++ b/src/main/java/io/temporal/internal/testservice/SelfAdvancingTimerImpl.java @@ -365,7 +365,7 @@ private void unlockTimeSkippingLocked(String caller) { private void unlockTimeSkippingLockedInternal() { if (lockCount == 0) { - throw new IllegalStateException("Unbalanced lock and unlock calls"); + throw new IllegalStateException("Unbalanced lock and unlock calls: \n" + getDiagnostics()); } lockCount--; if (lockCount == 0) { @@ -373,4 +373,10 @@ private void unlockTimeSkippingLockedInternal() { systemTimeLastLocked = -1; } } + + private String getDiagnostics() { + StringBuilder result = new StringBuilder(); + getDiagnostics(result); + return result.toString(); + } } diff --git a/src/main/java/io/temporal/internal/testservice/StateMachines.java b/src/main/java/io/temporal/internal/testservice/StateMachines.java index 8d86c66771..bb99924fb3 100644 --- a/src/main/java/io/temporal/internal/testservice/StateMachines.java +++ b/src/main/java/io/temporal/internal/testservice/StateMachines.java @@ -30,6 +30,7 @@ import io.temporal.internal.testservice.TestWorkflowStore.TaskListId; import io.temporal.proto.common.Payloads; import io.temporal.proto.common.RetryPolicy; +import io.temporal.proto.common.RetryStatus; import io.temporal.proto.common.TimeoutType; import io.temporal.proto.common.WorkflowExecution; import io.temporal.proto.decision.CancelTimerDecisionAttributes; @@ -52,6 +53,7 @@ import io.temporal.proto.event.ActivityTaskScheduledEventAttributes; import io.temporal.proto.event.ActivityTaskStartedEventAttributes; import io.temporal.proto.event.ActivityTaskTimedOutEventAttributes; +import io.temporal.proto.event.CancelExternalWorkflowExecutionFailedCause; import io.temporal.proto.event.ChildWorkflowExecutionCanceledEventAttributes; import io.temporal.proto.event.ChildWorkflowExecutionCompletedEventAttributes; import io.temporal.proto.event.ChildWorkflowExecutionFailedEventAttributes; @@ -69,8 +71,10 @@ import io.temporal.proto.event.HistoryEvent; import io.temporal.proto.event.RequestCancelExternalWorkflowExecutionFailedEventAttributes; import io.temporal.proto.event.RequestCancelExternalWorkflowExecutionInitiatedEventAttributes; +import io.temporal.proto.event.SignalExternalWorkflowExecutionFailedCause; import io.temporal.proto.event.SignalExternalWorkflowExecutionFailedEventAttributes; import io.temporal.proto.event.SignalExternalWorkflowExecutionInitiatedEventAttributes; +import io.temporal.proto.event.StartChildWorkflowExecutionFailedCause; import io.temporal.proto.event.StartChildWorkflowExecutionFailedEventAttributes; import io.temporal.proto.event.StartChildWorkflowExecutionInitiatedEventAttributes; import io.temporal.proto.event.TimerCanceledEventAttributes; @@ -80,10 +84,12 @@ import io.temporal.proto.event.WorkflowExecutionCanceledEventAttributes; import io.temporal.proto.event.WorkflowExecutionCompletedEventAttributes; import io.temporal.proto.event.WorkflowExecutionContinuedAsNewEventAttributes; -import io.temporal.proto.event.WorkflowExecutionFailedCause; import io.temporal.proto.event.WorkflowExecutionFailedEventAttributes; import io.temporal.proto.event.WorkflowExecutionStartedEventAttributes; import io.temporal.proto.event.WorkflowExecutionTimedOutEventAttributes; +import io.temporal.proto.failure.ApplicationFailureInfo; +import io.temporal.proto.failure.Failure; +import io.temporal.proto.failure.TimeoutFailureInfo; import io.temporal.proto.query.WorkflowQueryResult; import io.temporal.proto.workflowservice.GetWorkflowExecutionHistoryRequest; import io.temporal.proto.workflowservice.PollForActivityTaskRequest; @@ -121,7 +127,6 @@ class StateMachines { private static final Logger log = LoggerFactory.getLogger(StateMachines.class); static final int NO_EVENT_ID = -1; - private static final String TIMEOUT_ERROR_REASON = "temporalInternal:Timeout"; public static final int DEFAULT_WORKFLOW_EXECUTION_TIMEOUT_SECONDS = 10 * 365 * 24 * 3600; public static final int DEFAULT_WORKFLOW_TASK_TIMEOUT_SECONDS = 10; public static final int MAX_WORKFLOW_TASK_TIMEOUT_SECONDS = 60; @@ -522,7 +527,7 @@ public static StateMachine newCancelExternalStateMachine() { private static void noop(RequestContext ctx, T data, A a, long notUsed) {} private static void timeoutChildWorkflow( - RequestContext ctx, ChildWorkflowData data, TimeoutType timeoutType, long notUsed) { + RequestContext ctx, ChildWorkflowData data, RetryStatus retryStatus, long notUsed) { StartChildWorkflowExecutionInitiatedEventAttributes ie = data.initiatedEvent; ChildWorkflowExecutionTimedOutEventAttributes a = ChildWorkflowExecutionTimedOutEventAttributes.newBuilder() @@ -530,7 +535,7 @@ private static void timeoutChildWorkflow( .setStartedEventId(data.startedEventId) .setWorkflowExecution(data.execution) .setWorkflowType(ie.getWorkflowType()) - .setTimeoutType(timeoutType) + .setRetryStatus(retryStatus) .setInitiatedEventId(data.initiatedEventId) .build(); HistoryEvent event = @@ -727,7 +732,7 @@ private static void addStartChildTask( StartChildWorkflowExecutionFailedEventAttributes failRequest = StartChildWorkflowExecutionFailedEventAttributes.newBuilder() .setInitiatedEventId(initiatedEventId) - .setCause(WorkflowExecutionFailedCause.WorkflowAlreadyRunning) + .setCause(StartChildWorkflowExecutionFailedCause.WorkflowAlreadyExists) .build(); try { ctx.getWorkflowMutableState() @@ -881,9 +886,10 @@ private static void failWorkflow( long decisionTaskCompletedEventId) { WorkflowExecutionFailedEventAttributes.Builder a = WorkflowExecutionFailedEventAttributes.newBuilder() - .setReason(d.getReason()) - .setDetails(d.getDetails()) .setDecisionTaskCompletedEventId(decisionTaskCompletedEventId); + if (d.hasFailure()) { + a.setFailure(d.getFailure()); + } HistoryEvent event = HistoryEvent.newBuilder() .setEventType(EventType.WorkflowExecutionFailed) @@ -893,9 +899,9 @@ private static void failWorkflow( } private static void timeoutWorkflow( - RequestContext ctx, WorkflowData data, TimeoutType timeoutType, long notUsed) { + RequestContext ctx, WorkflowData data, RetryStatus retryStatus, long notUsed) { WorkflowExecutionTimedOutEventAttributes.Builder a = - WorkflowExecutionTimedOutEventAttributes.newBuilder().setTimeoutType(timeoutType); + WorkflowExecutionTimedOutEventAttributes.newBuilder().setRetryStatus(retryStatus); HistoryEvent event = HistoryEvent.newBuilder() .setEventType(EventType.WorkflowExecutionTimedOut) @@ -1094,9 +1100,6 @@ private static void scheduleQueryDecisionTask( ctx.onCommit( (historySize) -> { if (data.lastSuccessfulStartedEventId > 0) { - log.info( - "scheduleQueryDecisionTask lastSuccessfulStartedEventId=" - + data.lastSuccessfulStartedEventId); decisionTaskResponse.setPreviousStartedEventId(data.lastSuccessfulStartedEventId); } data.scheduledEventId = NO_EVENT_ID; @@ -1346,10 +1349,11 @@ private static void failDecisionTask( DecisionTaskFailedEventAttributes.Builder a = DecisionTaskFailedEventAttributes.newBuilder() .setIdentity(request.getIdentity()) - .setCause(request.getCause()) - .setDetails(request.getDetails()) .setStartedEventId(data.startedEventId) .setScheduledEventId(data.scheduledEventId); + if (request.hasFailure()) { + a.setFailure(request.getFailure()); + } HistoryEvent event = HistoryEvent.newBuilder() .setEventType(EventType.DecisionTaskFailed) @@ -1444,15 +1448,20 @@ private static State failActivityTask( private static State failActivityTaskByTaskToken( RequestContext ctx, ActivityTaskData data, RespondActivityTaskFailedRequest request) { - if (attemptActivityRetry(ctx, request.getReason(), data)) { + if (!request.getFailure().hasApplicationFailureInfo()) { + throw new IllegalArgumentException("application failure expected: " + request.getFailure()); + } + ApplicationFailureInfo info = request.getFailure().getApplicationFailureInfo(); + RetryStatus retryStatus = attemptActivityRetry(ctx, Optional.of(info), data); + if (retryStatus == RetryStatus.InProgress) { return INITIATED; } ActivityTaskFailedEventAttributes.Builder a = ActivityTaskFailedEventAttributes.newBuilder() .setIdentity(request.getIdentity()) .setScheduledEventId(data.scheduledEventId) - .setDetails(request.getDetails()) - .setReason(request.getReason()) + .setFailure(request.getFailure()) + .setRetryStatus(retryStatus) .setIdentity(request.getIdentity()) .setStartedEventId(data.startedEventId); HistoryEvent event = @@ -1466,15 +1475,20 @@ private static State failActivityTaskByTaskToken( private static State failActivityTaskById( RequestContext ctx, ActivityTaskData data, RespondActivityTaskFailedByIdRequest request) { - if (attemptActivityRetry(ctx, request.getReason(), data)) { + if (!request.getFailure().hasApplicationFailureInfo()) { + throw new IllegalArgumentException("application failure expected: " + request.getFailure()); + } + ApplicationFailureInfo info = request.getFailure().getApplicationFailureInfo(); + RetryStatus retryStatus = attemptActivityRetry(ctx, Optional.of(info), data); + if (retryStatus == RetryStatus.InProgress) { return INITIATED; } ActivityTaskFailedEventAttributes.Builder a = ActivityTaskFailedEventAttributes.newBuilder() .setIdentity(request.getIdentity()) .setScheduledEventId(data.scheduledEventId) - .setDetails(request.getDetails()) - .setReason(request.getReason()) + .setFailure(request.getFailure()) + .setRetryStatus(retryStatus) .setIdentity(request.getIdentity()) .setStartedEventId(data.startedEventId); HistoryEvent event = @@ -1488,20 +1502,35 @@ private static State failActivityTaskById( private static State timeoutActivityTask( RequestContext ctx, ActivityTaskData data, TimeoutType timeoutType, long notUsed) { - // ScheduleToStart (queue timeout) is not retriable. Instead of the retry, a customer should set + // ScheduleToStart (queue timeout) is not retryable. Instead of the retry, a customer should set // a larger ScheduleToStart timeout. - if (timeoutType != TimeoutType.ScheduleToStart - && attemptActivityRetry(ctx, TIMEOUT_ERROR_REASON, data)) { - return INITIATED; + RetryStatus retryStatus; + if (timeoutType != TimeoutType.ScheduleToStart) { + retryStatus = attemptActivityRetry(ctx, Optional.empty(), data); + if (retryStatus == RetryStatus.InProgress) { + return INITIATED; + } + } else { + retryStatus = RetryStatus.NonRetryableFailure; + } + Failure failure; + if (timeoutType == TimeoutType.Heartbeat || timeoutType == TimeoutType.Heartbeat.StartToClose) { + failure = + newTimeoutFailure( + TimeoutType.ScheduleToClose, + Optional.ofNullable(data.heartbeatDetails), + Optional.of(newTimeoutFailure(timeoutType, Optional.empty(), Optional.empty()))); + } else { + failure = + newTimeoutFailure( + timeoutType, Optional.ofNullable(data.heartbeatDetails), Optional.empty()); } ActivityTaskTimedOutEventAttributes.Builder a = ActivityTaskTimedOutEventAttributes.newBuilder() .setScheduledEventId(data.scheduledEventId) - .setTimeoutType(timeoutType) - .setStartedEventId(data.startedEventId); - if (data.heartbeatDetails != null) { - a.setDetails(data.heartbeatDetails); - } + .setRetryStatus(retryStatus) + .setStartedEventId(data.startedEventId) + .setFailure(failure); HistoryEvent event = HistoryEvent.newBuilder() .setEventType(EventType.ActivityTaskTimedOut) @@ -1511,29 +1540,48 @@ && attemptActivityRetry(ctx, TIMEOUT_ERROR_REASON, data)) { return TIMED_OUT; } - private static boolean attemptActivityRetry( - RequestContext ctx, String errorReason, ActivityTaskData data) { - if (data.retryState != null) { - RetryState nextAttempt = data.retryState.getNextAttempt(); - data.nextBackoffIntervalSeconds = - data.retryState.getBackoffIntervalInSeconds(errorReason, data.store.currentTimeMillis()); - if (data.nextBackoffIntervalSeconds > 0) { - PollForActivityTaskResponse.Builder task = data.activityTask.getTask(); - if (data.heartbeatDetails != null) { - task.setHeartbeatDetails(data.heartbeatDetails); - } - ctx.onCommit( - (historySize) -> { - data.retryState = nextAttempt; - task.setAttempt(nextAttempt.getAttempt()); - task.setScheduledTimestampOfThisAttempt(ctx.currentTimeInNanoseconds()); - }); - return true; - } else { - data.startedEventId = ctx.addEvent(data.startedEvent); + private static Failure newTimeoutFailure( + TimeoutType timeoutType, Optional lastHeartbeatDetails, Optional cause) { + TimeoutFailureInfo.Builder info = TimeoutFailureInfo.newBuilder().setTimeoutType(timeoutType); + if (lastHeartbeatDetails.isPresent()) { + info.setLastHeartbeatDetails(lastHeartbeatDetails.get()); + } + Failure.Builder result = Failure.newBuilder().setTimeoutFailureInfo(info); + if (cause.isPresent()) { + result.setCause(cause.get()); + } + return result.build(); + } + + private static RetryStatus attemptActivityRetry( + RequestContext ctx, Optional info, ActivityTaskData data) { + if (data.retryState == null) { + return RetryStatus.RetryPolicyNotSet; + } + if (info.isPresent() && info.get().getNonRetryable()) { + return RetryStatus.NonRetryableFailure; + } + RetryState nextAttempt = data.retryState.getNextAttempt(); + RetryState.BackoffInterval backoffInterval = + data.retryState.getBackoffIntervalInSeconds( + info.map(i -> i.getType()), data.store.currentTimeMillis()); + if (backoffInterval.getRetryStatus() == RetryStatus.InProgress) { + data.nextBackoffIntervalSeconds = backoffInterval.getIntervalSeconds(); + PollForActivityTaskResponse.Builder task = data.activityTask.getTask(); + if (data.heartbeatDetails != null) { + task.setHeartbeatDetails(data.heartbeatDetails); } + ctx.onCommit( + (historySize) -> { + data.retryState = nextAttempt; + task.setAttempt(nextAttempt.getAttempt()); + task.setScheduledTimestampOfThisAttempt(ctx.currentTimeInNanoseconds()); + }); + } else { + data.startedEventId = ctx.addEvent(data.startedEvent); + data.nextBackoffIntervalSeconds = 0; } - return false; + return backoffInterval.getRetryStatus(); } private static void reportActivityTaskCancellation( @@ -1660,7 +1708,7 @@ private static void initiateExternalSignal( private static void failExternalSignal( RequestContext ctx, SignalExternalData data, - WorkflowExecutionFailedCause cause, + SignalExternalWorkflowExecutionFailedCause cause, long notUsed) { SignalExternalWorkflowExecutionInitiatedEventAttributes initiatedEvent = data.initiatedEvent; SignalExternalWorkflowExecutionFailedEventAttributes.Builder a = @@ -1751,7 +1799,7 @@ private static void reportExternalCancellationRequested( private static void failExternalCancellation( RequestContext ctx, CancelExternalData data, - WorkflowExecutionFailedCause cause, + CancelExternalWorkflowExecutionFailedCause cause, long notUsed) { RequestCancelExternalWorkflowExecutionInitiatedEventAttributes initiatedEvent = data.initiatedEvent; diff --git a/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableState.java b/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableState.java index e019e3fc23..6f67e24626 100644 --- a/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableState.java +++ b/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableState.java @@ -28,8 +28,8 @@ import io.temporal.proto.event.ChildWorkflowExecutionStartedEventAttributes; import io.temporal.proto.event.ChildWorkflowExecutionTimedOutEventAttributes; import io.temporal.proto.event.ExternalWorkflowExecutionCancelRequestedEventAttributes; +import io.temporal.proto.event.SignalExternalWorkflowExecutionFailedCause; import io.temporal.proto.event.StartChildWorkflowExecutionFailedEventAttributes; -import io.temporal.proto.event.WorkflowExecutionFailedCause; import io.temporal.proto.execution.WorkflowExecutionStatus; import io.temporal.proto.workflowservice.PollForActivityTaskRequest; import io.temporal.proto.workflowservice.PollForActivityTaskResponseOrBuilder; @@ -68,7 +68,8 @@ void startDecisionTask( void completeSignalExternalWorkflowExecution(String signalId, String runId); - void failSignalExternalWorkflowExecution(String signalId, WorkflowExecutionFailedCause cause); + void failSignalExternalWorkflowExecution( + String signalId, SignalExternalWorkflowExecutionFailedCause cause); void failDecisionTask(RespondDecisionTaskFailedRequest request); diff --git a/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableStateImpl.java b/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableStateImpl.java index 870d36a2f3..39bac10bc4 100644 --- a/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableStateImpl.java +++ b/src/main/java/io/temporal/internal/testservice/TestWorkflowMutableStateImpl.java @@ -36,6 +36,7 @@ import io.temporal.internal.common.WorkflowExecutionUtils; import io.temporal.internal.testservice.StateMachines.*; import io.temporal.proto.common.Payloads; +import io.temporal.proto.common.RetryStatus; import io.temporal.proto.common.TimeoutType; import io.temporal.proto.common.WorkflowExecution; import io.temporal.proto.decision.CancelTimerDecisionAttributes; @@ -66,12 +67,13 @@ import io.temporal.proto.event.ExternalWorkflowExecutionCancelRequestedEventAttributes; import io.temporal.proto.event.HistoryEvent; import io.temporal.proto.event.MarkerRecordedEventAttributes; +import io.temporal.proto.event.SignalExternalWorkflowExecutionFailedCause; import io.temporal.proto.event.StartChildWorkflowExecutionFailedEventAttributes; import io.temporal.proto.event.UpsertWorkflowSearchAttributesEventAttributes; import io.temporal.proto.event.WorkflowExecutionContinuedAsNewEventAttributes; -import io.temporal.proto.event.WorkflowExecutionFailedCause; import io.temporal.proto.event.WorkflowExecutionSignaledEventAttributes; import io.temporal.proto.execution.WorkflowExecutionStatus; +import io.temporal.proto.failure.ApplicationFailureInfo; import io.temporal.proto.query.QueryConsistencyLevel; import io.temporal.proto.query.QueryRejectCondition; import io.temporal.proto.query.QueryRejected; @@ -632,9 +634,14 @@ private void processRecordMarker( MarkerRecordedEventAttributes.Builder marker = MarkerRecordedEventAttributes.newBuilder() .setMarkerName(attr.getMarkerName()) - .setHeader(attr.getHeader()) - .setDetails(attr.getDetails()) - .setDecisionTaskCompletedEventId(decisionTaskCompletedId); + .setDecisionTaskCompletedEventId(decisionTaskCompletedId) + .putAllDetails(attr.getDetailsMap()); + if (attr.hasHeader()) { + marker.setHeader(attr.getHeader()); + } + if (attr.hasFailure()) { + marker.setFailure(attr.getFailure()); + } HistoryEvent event = HistoryEvent.newBuilder() .setEventType(EventType.MarkerRecorded) @@ -689,8 +696,8 @@ private void processScheduleActivityTask( RequestContext ctx, ScheduleActivityTaskDecisionAttributes a, long decisionTaskCompletedId) { a = validateScheduleActivityTask(a); String activityId = a.getActivityId(); - Long activityscheduledEventId = activityById.get(activityId); - if (activityscheduledEventId != null) { + Long activityScheduledEventId = activityById.get(activityId); + if (activityScheduledEventId != null) { throw Status.FAILED_PRECONDITION .withDescription("Already open activity with " + activityId) .asRuntimeException(); @@ -892,7 +899,7 @@ public void completeSignalExternalWorkflowExecution(String signalId, String runI @Override public void failSignalExternalWorkflowExecution( - String signalId, WorkflowExecutionFailedCause cause) { + String signalId, SignalExternalWorkflowExecutionFailedCause cause) { update( ctx -> { StateMachine signal = getSignal(signalId); @@ -992,7 +999,7 @@ public void childWorkflowTimedOut( update( ctx -> { StateMachine child = getChildWorkflow(a.getInitiatedEventId()); - child.action(Action.TIME_OUT, ctx, a.getTimeoutType(), 0); + child.action(Action.TIME_OUT, ctx, a.getRetryStatus(), 0); childWorkflows.remove(a.getInitiatedEventId()); scheduleDecision(ctx); ctx.unlockTimer(); @@ -1094,16 +1101,27 @@ private void processFailWorkflowExecution( WorkflowData data = workflow.getData(); if (data.retryState.isPresent()) { RetryState rs = data.retryState.get(); - int backoffIntervalSeconds = - rs.getBackoffIntervalInSeconds(d.getReason(), store.currentTimeMillis()); - if (backoffIntervalSeconds > 0) { + Optional failureType; + RetryState.BackoffInterval backoffInterval; + if (d.getFailure().hasApplicationFailureInfo()) { + ApplicationFailureInfo failureInfo = d.getFailure().getApplicationFailureInfo(); + if (failureInfo.getNonRetryable()) { + backoffInterval = new RetryState.BackoffInterval(RetryStatus.NonRetryableFailure); + } else { + failureType = Optional.of(failureInfo.getType()); + backoffInterval = rs.getBackoffIntervalInSeconds(failureType, store.currentTimeMillis()); + } + } else { + backoffInterval = new RetryState.BackoffInterval(RetryStatus.NonRetryableFailure); + } + if (backoffInterval.getRetryStatus() == RetryStatus.InProgress) { ContinueAsNewWorkflowExecutionDecisionAttributes.Builder continueAsNewAttr = ContinueAsNewWorkflowExecutionDecisionAttributes.newBuilder() .setInput(startRequest.getInput()) .setWorkflowType(startRequest.getWorkflowType()) .setWorkflowRunTimeoutSeconds(startRequest.getWorkflowRunTimeoutSeconds()) .setWorkflowTaskTimeoutSeconds(startRequest.getWorkflowTaskTimeoutSeconds()) - .setBackoffStartIntervalInSeconds(backoffIntervalSeconds); + .setBackoffStartIntervalInSeconds(backoffInterval.getIntervalSeconds()); if (startRequest.hasTaskList()) { continueAsNewAttr.setTaskList(startRequest.getTaskList()); } @@ -1149,8 +1167,7 @@ private void processFailWorkflowExecution( ChildWorkflowExecutionFailedEventAttributes a = ChildWorkflowExecutionFailedEventAttributes.newBuilder() .setInitiatedEventId(parentChildInitiatedEventId.getAsLong()) - .setDetails(d.getDetails()) - .setReason(d.getReason()) + .setFailure(d.getFailure()) .setWorkflowType(startRequest.getWorkflowType()) .setNamespace(ctx.getNamespace()) .setWorkflowExecution(ctx.getExecution()) @@ -1373,13 +1390,11 @@ public void startWorkflow( scheduleDecision(ctx); } - int executionTimeoutTimerDelay = startRequest.getWorkflowRunTimeoutSeconds(); + int runTimeoutSeconds = startRequest.getWorkflowRunTimeoutSeconds(); if (backoffStartIntervalInSeconds > 0) { - executionTimeoutTimerDelay = - executionTimeoutTimerDelay + backoffStartIntervalInSeconds; + runTimeoutSeconds = runTimeoutSeconds + backoffStartIntervalInSeconds; } - ctx.addTimer( - executionTimeoutTimerDelay, this::timeoutWorkflow, "workflow execution timeout"); + ctx.addTimer(runTimeoutSeconds, this::timeoutWorkflow, "workflow execution timeout"); }); } catch (StatusRuntimeException e) { if (e.getStatus().getCode() == Status.Code.NOT_FOUND) { @@ -1430,10 +1445,10 @@ public void startActivityTask( int heartbeatTimeout = data.scheduledEvent.getHeartbeatTimeoutSeconds(); long scheduledEventId = activity.getData().scheduledEventId; if (startToCloseTimeout > 0) { + int attempt = data.getAttempt(); ctx.addTimer( startToCloseTimeout, - () -> - timeoutActivity(scheduledEventId, TimeoutType.StartToClose, data.getAttempt()), + () -> timeoutActivity(scheduledEventId, TimeoutType.StartToClose, attempt), "Activity StartToCloseTimeout"); } updateHeartbeatTimer( @@ -1473,9 +1488,10 @@ private void updateHeartbeatTimer( if (heartbeatTimeout > 0 && heartbeatTimeout < startToCloseTimeout) { ActivityTaskData data = activity.getData(); data.lastHeartbeatTime = clock.getAsLong(); + int attempt = data.getAttempt(); ctx.addTimer( heartbeatTimeout, - () -> timeoutActivity(activityId, TimeoutType.Heartbeat, data.getAttempt()), + () -> timeoutActivity(activityId, TimeoutType.Heartbeat, attempt), "Activity Heartbeat Timeout"); } } @@ -1683,6 +1699,7 @@ private void timeoutActivity(long scheduledEventId, TimeoutType timeoutType, int } } + // TODO(maxim): Add workflow retry on run timeout private void timeoutWorkflow() { lock.lock(); try { @@ -1700,7 +1717,8 @@ private void timeoutWorkflow() { if (isTerminalState(workflow.getState())) { return; } - workflow.action(StateMachines.Action.TIME_OUT, ctx, TimeoutType.StartToClose, 0); + // TODO(maxim): real retry status + workflow.action(StateMachines.Action.TIME_OUT, ctx, RetryStatus.Timeout, 0); decision.getData().workflowCompleted = true; if (parent != null) { ctx.lockTimer(); // unlocked by the parent @@ -1721,7 +1739,7 @@ private void reportWorkflowTimeoutToParent(RequestContext ctx) { ChildWorkflowExecutionTimedOutEventAttributes a = ChildWorkflowExecutionTimedOutEventAttributes.newBuilder() .setInitiatedEventId(parentChildInitiatedEventId.getAsLong()) - .setTimeoutType(TimeoutType.StartToClose) + .setRetryStatus(RetryStatus.Timeout) // TODO(maxim): Real status .setWorkflowType(startRequest.getWorkflowType()) .setNamespace(ctx.getNamespace()) .setWorkflowExecution(ctx.getExecution()) @@ -2033,7 +2051,7 @@ private StateMachine getActivityById(String activityId) { } private void removeActivity(long scheduledEventId) { - StateMachine activity = activities.get(scheduledEventId); + StateMachine activity = activities.remove(scheduledEventId); if (activity == null) { return; } diff --git a/src/main/java/io/temporal/internal/testservice/TestWorkflowService.java b/src/main/java/io/temporal/internal/testservice/TestWorkflowService.java index ff483f1143..dc2f2eadd4 100644 --- a/src/main/java/io/temporal/internal/testservice/TestWorkflowService.java +++ b/src/main/java/io/temporal/internal/testservice/TestWorkflowService.java @@ -37,8 +37,8 @@ import io.temporal.proto.common.WorkflowIdReusePolicy; import io.temporal.proto.decision.SignalExternalWorkflowExecutionDecisionAttributes; import io.temporal.proto.errordetails.WorkflowExecutionAlreadyStartedFailure; +import io.temporal.proto.event.SignalExternalWorkflowExecutionFailedCause; import io.temporal.proto.event.WorkflowExecutionContinuedAsNewEventAttributes; -import io.temporal.proto.event.WorkflowExecutionFailedCause; import io.temporal.proto.execution.WorkflowExecutionInfo; import io.temporal.proto.execution.WorkflowExecutionStatus; import io.temporal.proto.workflowservice.GetWorkflowExecutionHistoryRequest; @@ -163,7 +163,7 @@ public void close() { try { channel.awaitTermination(1, TimeUnit.SECONDS); } catch (InterruptedException e) { - e.printStackTrace(); + log.debug("interrupted", e); } store.close(); } @@ -803,7 +803,8 @@ public void signalExternalWorkflowExecution( } catch (StatusRuntimeException e) { if (e.getStatus().getCode() == Status.Code.NOT_FOUND) { source.failSignalExternalWorkflowExecution( - signalId, WorkflowExecutionFailedCause.UnknownExternalWorkflowExecution); + signalId, + SignalExternalWorkflowExecutionFailedCause.ExternalWorkflowExecutionNotFound2); } else { throw e; } diff --git a/src/main/java/io/temporal/internal/testservice/TestWorkflowStoreImpl.java b/src/main/java/io/temporal/internal/testservice/TestWorkflowStoreImpl.java index 26affd1ccd..70527838dc 100644 --- a/src/main/java/io/temporal/internal/testservice/TestWorkflowStoreImpl.java +++ b/src/main/java/io/temporal/internal/testservice/TestWorkflowStoreImpl.java @@ -458,7 +458,7 @@ public void getDiagnostics(StringBuilder result) { lock.unlock(); } // Uncomment to troubleshoot time skipping issues. - timerService.getDiagnostics(result); + // timerService.getDiagnostics(result); } @Override diff --git a/src/main/java/io/temporal/internal/worker/ActivityWorker.java b/src/main/java/io/temporal/internal/worker/ActivityWorker.java index 9a08069bf5..5024e45ebf 100644 --- a/src/main/java/io/temporal/internal/worker/ActivityWorker.java +++ b/src/main/java/io/temporal/internal/worker/ActivityWorker.java @@ -24,16 +24,17 @@ import com.uber.m3.util.Duration; import com.uber.m3.util.ImmutableMap; import io.temporal.common.context.ContextPropagator; -import io.temporal.common.converter.GsonJsonDataConverter; import io.temporal.internal.common.GrpcRetryer; -import io.temporal.internal.common.OptionsUtils; import io.temporal.internal.common.RpcRetryOptions; import io.temporal.internal.logging.LoggerTag; import io.temporal.internal.metrics.MetricsTag; import io.temporal.internal.metrics.MetricsType; +import io.temporal.internal.replay.FailureWrapperException; import io.temporal.internal.worker.ActivityTaskHandler.Result; import io.temporal.proto.common.Payload; import io.temporal.proto.common.WorkflowExecution; +import io.temporal.proto.failure.CanceledFailureInfo; +import io.temporal.proto.failure.Failure; import io.temporal.proto.workflowservice.PollForActivityTaskResponse; import io.temporal.proto.workflowservice.RespondActivityTaskCanceledRequest; import io.temporal.proto.workflowservice.RespondActivityTaskCompletedRequest; @@ -42,7 +43,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; import org.slf4j.MDC; @@ -192,17 +192,19 @@ public void handle(PollForActivityTaskResponse task) throws Exception { Duration duration = Duration.ofNanos(nanoTime - task.getScheduledTimestampOfThisAttempt()); metricsScope.timer(MetricsType.ACTIVITY_E2E_LATENCY).record(duration); - } catch (CancellationException e) { - RespondActivityTaskCanceledRequest cancelledRequest = - RespondActivityTaskCanceledRequest.newBuilder() - .setDetails( - GsonJsonDataConverter.getInstance() - .toData(OptionsUtils.safeGet(e.getMessage())) - .get()) - .build(); - Stopwatch sw = metricsScope.timer(MetricsType.ACTIVITY_RESP_LATENCY).start(); - sendReply(task, new Result(null, null, cancelledRequest, null), metricsScope); - sw.stop(); + } catch (FailureWrapperException e) { + Failure failure = e.getFailure(); + if (failure.hasCanceledFailureInfo()) { + CanceledFailureInfo info = failure.getCanceledFailureInfo(); + RespondActivityTaskCanceledRequest.Builder cancelledRequest = + RespondActivityTaskCanceledRequest.newBuilder().setIdentity(options.getIdentity()); + if (info.hasDetails()) { + cancelledRequest.setDetails(info.getDetails()); + } + Stopwatch sw = metricsScope.timer(MetricsType.ACTIVITY_RESP_LATENCY).start(); + sendReply(task, new Result(null, null, cancelledRequest.build(), null), metricsScope); + sw.stop(); + } } finally { MDC.remove(LoggerTag.ACTIVITY_ID); MDC.remove(LoggerTag.ACTIVITY_TYPE); diff --git a/src/main/java/io/temporal/internal/worker/LocalActivityWorker.java b/src/main/java/io/temporal/internal/worker/LocalActivityWorker.java index 101e12abfc..189a6bbd4a 100644 --- a/src/main/java/io/temporal/internal/worker/LocalActivityWorker.java +++ b/src/main/java/io/temporal/internal/worker/LocalActivityWorker.java @@ -26,12 +26,8 @@ import io.temporal.internal.common.LocalActivityMarkerData; import io.temporal.internal.metrics.MetricsTag; import io.temporal.internal.metrics.MetricsType; -import io.temporal.internal.replay.ClockDecisionContext; import io.temporal.internal.replay.ExecuteLocalActivityParameters; -import io.temporal.proto.common.Payloads; -import io.temporal.proto.event.EventType; import io.temporal.proto.event.HistoryEvent; -import io.temporal.proto.event.MarkerRecordedEventAttributes; import io.temporal.proto.workflowservice.PollForActivityTaskResponse; import io.temporal.proto.workflowservice.RespondActivityTaskCompletedRequest; import java.time.Duration; @@ -205,33 +201,18 @@ public void handle(Task task) throws Exception { RespondActivityTaskCompletedRequest taskCompleted = result.getTaskCompleted(); if (taskCompleted != null) { - Optional r = - taskCompleted.hasResult() ? Optional.of(taskCompleted.getResult()) : Optional.empty(); - markerBuilder.setResult(r); + if (taskCompleted.hasResult()) { + markerBuilder.setResult(taskCompleted.getResult()); + } } else if (result.getTaskFailed() != null) { markerBuilder.setTaskFailedRequest(result.getTaskFailed().getTaskFailedRequest()); markerBuilder.setAttempt(result.getAttempt()); markerBuilder.setBackoff(result.getBackoff()); } else { - markerBuilder.setTaskCancelledRequest( - result.getTaskCancelled(), options.getDataConverter()); + markerBuilder.setTaskCancelledRequest(result.getTaskCancelled()); } - LocalActivityMarkerData marker = markerBuilder.build(); - - MarkerRecordedEventAttributes.Builder attributes = - MarkerRecordedEventAttributes.newBuilder() - .setMarkerName(ClockDecisionContext.LOCAL_ACTIVITY_MARKER_NAME) - .setHeader(marker.getHeader(options.getDataConverter().getPayloadConverter())); - Optional r = marker.getResult(); - if (r.isPresent()) { - attributes.setDetails(r.get()); - } - HistoryEvent event = - HistoryEvent.newBuilder() - .setEventType(EventType.MarkerRecorded) - .setMarkerRecordedEventAttributes(attributes) - .build(); + HistoryEvent event = marker.toEvent(options.getDataConverter()); task.eventConsumer.accept(event); } diff --git a/src/main/java/io/temporal/internal/worker/PollDecisionTaskDispatcher.java b/src/main/java/io/temporal/internal/worker/PollDecisionTaskDispatcher.java index f53cee1fc8..ed008caa57 100644 --- a/src/main/java/io/temporal/internal/worker/PollDecisionTaskDispatcher.java +++ b/src/main/java/io/temporal/internal/worker/PollDecisionTaskDispatcher.java @@ -19,7 +19,7 @@ package io.temporal.internal.worker; -import io.temporal.common.converter.GsonJsonDataConverter; +import io.temporal.failure.FailureConverter; import io.temporal.proto.event.DecisionTaskFailedCause; import io.temporal.proto.workflowservice.PollForDecisionTaskResponse; import io.temporal.proto.workflowservice.RespondDecisionTaskFailedRequest; @@ -66,17 +66,18 @@ public void process(PollForDecisionTaskResponse t) { if (subscribers.containsKey(taskListName)) { subscribers.get(taskListName).accept(t); } else { - String message = - String.format( - "No handler is subscribed for the PollForDecisionTaskResponse.WorkflowExecutionTaskList %s", - taskListName); + Exception exception = + new Exception( + String.format( + "No handler is subscribed for the PollForDecisionTaskResponse.WorkflowExecutionTaskList %s", + taskListName)); RespondDecisionTaskFailedRequest request = RespondDecisionTaskFailedRequest.newBuilder() .setTaskToken(t.getTaskToken()) .setCause(DecisionTaskFailedCause.ResetStickyTasklist) - .setDetails(GsonJsonDataConverter.getInstance().toData(message).get()) + .setFailure(FailureConverter.exceptionToFailure(exception)) .build(); - log.warn(message); + log.warn("unexpected", exception); try { service.blockingStub().respondDecisionTaskFailed(request); diff --git a/src/main/java/io/temporal/internal/worker/SingleWorkerOptions.java b/src/main/java/io/temporal/internal/worker/SingleWorkerOptions.java index d348830f03..8c7a29f3eb 100644 --- a/src/main/java/io/temporal/internal/worker/SingleWorkerOptions.java +++ b/src/main/java/io/temporal/internal/worker/SingleWorkerOptions.java @@ -22,7 +22,6 @@ import com.uber.m3.tally.Scope; import io.temporal.common.context.ContextPropagator; import io.temporal.common.converter.DataConverter; -import io.temporal.common.converter.GsonJsonDataConverter; import io.temporal.internal.metrics.NoopScope; import java.time.Duration; import java.util.List; @@ -109,7 +108,7 @@ public SingleWorkerOptions build() { } if (dataConverter == null) { - dataConverter = GsonJsonDataConverter.getInstance(); + dataConverter = DataConverter.getDefaultInstance(); } if (metricsScope == null) { diff --git a/src/main/java/io/temporal/internal/worker/WorkflowExecutionException.java b/src/main/java/io/temporal/internal/worker/WorkflowExecutionException.java index 38befe13e6..69c95b45fe 100644 --- a/src/main/java/io/temporal/internal/worker/WorkflowExecutionException.java +++ b/src/main/java/io/temporal/internal/worker/WorkflowExecutionException.java @@ -19,23 +19,18 @@ package io.temporal.internal.worker; -import io.temporal.proto.common.Payloads; -import java.util.Optional; +import io.temporal.proto.failure.Failure; /** Internal. Do not throw or catch in application level code. */ public final class WorkflowExecutionException extends RuntimeException { - private final Optional details; + private final Failure failure; - public WorkflowExecutionException(String reason, Optional details) { - super(reason); - this.details = details; + public WorkflowExecutionException(Failure failure) { + super(failure.getMessage()); + this.failure = failure; } - public Optional getDetails() { - return details; - } - - public String getReason() { - return getMessage(); + public Failure getFailure() { + return failure; } } diff --git a/src/main/java/io/temporal/testing/SimulatedTimeoutException.java b/src/main/java/io/temporal/testing/SimulatedTimeoutException.java deleted file mode 100644 index 6571f6bb85..0000000000 --- a/src/main/java/io/temporal/testing/SimulatedTimeoutException.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. - * - * 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 io.temporal.testing; - -import com.google.common.annotations.VisibleForTesting; -import io.temporal.proto.common.TimeoutType; - -/** - * SimulatedTimeoutException can be thrown from an activity or child workflow implementation to - * simulate a timeout. To be used only in unit tests. If thrown from an activity the workflow code - * is going to receive it as {@link io.temporal.workflow.ActivityTimeoutException}. If thrown from a - * child workflow the workflow code is going to receive it as {@link - * io.temporal.workflow.ChildWorkflowTimedOutException}. - */ -@VisibleForTesting -public final class SimulatedTimeoutException extends RuntimeException { - - private final TimeoutType timeoutType; - - private final Object details; - - /** - * Creates an instance with specific timeoutType and details. Use this constructor to simulate an - * activity timeout. - * - * @param timeoutType timeout type to simulate - * @param details details included into the timeout exception. - */ - public SimulatedTimeoutException(TimeoutType timeoutType, Object details) { - this.timeoutType = timeoutType; - this.details = details; - } - - /** - * Creates an instance with no details and START_TO_CLOSE timeout. Use this constructor to - * simulate a child workflow timeout. - */ - public SimulatedTimeoutException() { - this.timeoutType = TimeoutType.StartToClose; - this.details = null; - } - - /** - * Creates an instance with specific timeoutType and empty details. Use this constructor to - * simulate an activity timeout. - * - * @param timeoutType timeout type to simulate - */ - public SimulatedTimeoutException(TimeoutType timeoutType) { - this.timeoutType = timeoutType; - this.details = null; - } - - public TimeoutType getTimeoutType() { - return timeoutType; - } - - public Object getDetails() { - return details; - } -} diff --git a/src/main/java/io/temporal/testing/SimulatedTimeoutFailure.java b/src/main/java/io/temporal/testing/SimulatedTimeoutFailure.java new file mode 100644 index 0000000000..80737abf70 --- /dev/null +++ b/src/main/java/io/temporal/testing/SimulatedTimeoutFailure.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.testing; + +import com.google.common.annotations.VisibleForTesting; +import io.temporal.failure.TimeoutFailure; + +/** Internal do not use in application code. */ +@VisibleForTesting +public final class SimulatedTimeoutFailure extends RuntimeException { + + public SimulatedTimeoutFailure(TimeoutFailure cause) { + super(null, cause, false, true); + setStackTrace(cause.getStackTrace()); + } +} diff --git a/src/main/java/io/temporal/workflow/ActivityException.java b/src/main/java/io/temporal/workflow/ActivityException.java deleted file mode 100644 index 84616b355e..0000000000 --- a/src/main/java/io/temporal/workflow/ActivityException.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. - * - * 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 io.temporal.workflow; - -import io.temporal.proto.common.ActivityType; - -/** Exception used to communicate failure of a remote activity. */ -@SuppressWarnings("serial") -public abstract class ActivityException extends WorkflowOperationException { - - private final ActivityType activityType; - - private final String activityId; - - ActivityException(String message, long eventId, ActivityType activityType, String activityId) { - super( - message - + ", ActivityType=\"" - + activityType.getName() - + "\", ActivityId=\"" - + activityId - + "\", EventId=" - + eventId, - eventId); - this.activityType = activityType; - this.activityId = activityId; - } - - public ActivityType getActivityType() { - return activityType; - } - - public String getActivityId() { - return activityId; - } -} diff --git a/src/main/java/io/temporal/workflow/ActivityFailureException.java b/src/main/java/io/temporal/workflow/ActivityFailureException.java deleted file mode 100644 index d0dd284dde..0000000000 --- a/src/main/java/io/temporal/workflow/ActivityFailureException.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. - * - * 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 io.temporal.workflow; - -import io.temporal.proto.common.ActivityType; -import java.time.Duration; - -/** - * Indicates that an activity implementation threw an unhandled exception. Contains the unhandled - * exception as a cause. Note that an unhandled exception stack trace might belong to a separate - * process or even program. - */ -public final class ActivityFailureException extends ActivityException { - private int attempt; - private Duration backoff; - - public ActivityFailureException( - long eventId, ActivityType activityType, String activityId, Throwable cause) { - super("ActivityFailureException", eventId, activityType, activityId); - initCause(cause); - } - - public ActivityFailureException( - long eventId, - ActivityType activityType, - String activityId, - Throwable cause, - int attempt, - Duration backoff) { - super( - "ActivityFailureException Attempt=\"" + attempt + "\", Backoff=\"" + backoff, - eventId, - activityType, - activityId); - initCause(cause); - this.attempt = attempt; - this.backoff = backoff; - } - - public Duration getBackoff() { - return backoff; - } - - public int getAttempt() { - return attempt; - } -} diff --git a/src/main/java/io/temporal/workflow/ActivityTimeoutException.java b/src/main/java/io/temporal/workflow/ActivityTimeoutException.java deleted file mode 100644 index 8f862a0d26..0000000000 --- a/src/main/java/io/temporal/workflow/ActivityTimeoutException.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. - * - * 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 io.temporal.workflow; - -import com.google.protobuf.InvalidProtocolBufferException; -import io.temporal.common.converter.DataConverter; -import io.temporal.common.converter.DataConverterException; -import io.temporal.proto.common.ActivityType; -import io.temporal.proto.common.Payloads; -import io.temporal.proto.common.TimeoutType; -import java.lang.reflect.Type; -import java.util.Objects; -import java.util.Optional; - -/** - * ActivityTimeoutException indicates that an activity has timed out. If the timeout type is a - * {@link TimeoutType#Heartbeat} then the {@link #getDetails(Class)} returns a value passed to the - * latest successful {@link io.temporal.activity.Activity#heartbeat(Object)} call. - */ -@SuppressWarnings("serial") -public final class ActivityTimeoutException extends ActivityException { - - private final TimeoutType timeoutType; - - private final byte[] details; - private DataConverter dataConverter; - - public ActivityTimeoutException( - long eventId, - ActivityType activityType, - String activityId, - TimeoutType timeoutType, - Optional details, - DataConverter dataConverter) { - super("TimeoutType=" + timeoutType, eventId, activityType, activityId); - this.timeoutType = Objects.requireNonNull(timeoutType); - // Serialize to byte array as the exception itself has to be serialized - this.details = details.isPresent() ? details.get().toByteArray() : null; - this.dataConverter = Objects.requireNonNull(dataConverter); - } - - public TimeoutType getTimeoutType() { - return timeoutType; - } - - /** @return The value from the last activity heartbeat details field. */ - public V getDetails(Class detailsClass) { - return getDetails(detailsClass, detailsClass); - } - - /** @return The value from the last activity heartbeat details field. */ - public V getDetails(Class detailsClass, Type detailsType) { - Optional payloads; - try { - payloads = details == null ? Optional.empty() : Optional.of(Payloads.parseFrom(details)); - } catch (InvalidProtocolBufferException e) { - throw new DataConverterException(e); - } - return dataConverter.fromData(payloads, detailsClass, detailsType); - } -} diff --git a/src/main/java/io/temporal/workflow/CancelExternalWorkflowException.java b/src/main/java/io/temporal/workflow/CancelExternalWorkflowException.java index cfa4f9bd33..0449ccc984 100644 --- a/src/main/java/io/temporal/workflow/CancelExternalWorkflowException.java +++ b/src/main/java/io/temporal/workflow/CancelExternalWorkflowException.java @@ -19,32 +19,31 @@ package io.temporal.workflow; +import io.temporal.client.WorkflowException; import io.temporal.proto.common.WorkflowExecution; -import io.temporal.proto.event.WorkflowExecutionFailedCause; /** - * Exception used to communicate failure of a request to signal an external workflow. TODO: Hook it - * up with RequestCancelExternalWorkflowExecutionFailed and WorkflowExecutionCancelRequested + * Exception used to communicate failure of a request to cancel an external workflow. + * + *

TODO: Hook it up with RequestCancelExternalWorkflowExecutionFailed and + * WorkflowExecutionCancelRequested */ @SuppressWarnings("serial") -public final class CancelExternalWorkflowException extends WorkflowOperationException { - - private WorkflowExecutionFailedCause failureCause; - - private WorkflowExecution signaledExecution; - - public CancelExternalWorkflowException( - long eventId, WorkflowExecution signaledExecution, WorkflowExecutionFailedCause cause) { - super(cause + " for signaledExecution=\"" + signaledExecution, eventId); - this.signaledExecution = signaledExecution; - this.failureCause = cause; - } +public final class CancelExternalWorkflowException extends WorkflowException { - public WorkflowExecutionFailedCause getFailureCause() { - return failureCause; + protected CancelExternalWorkflowException( + WorkflowExecution execution, String workflowType, Throwable cause) { + super(execution, workflowType, cause); } - public WorkflowExecution getSignaledExecution() { - return signaledExecution; + @Override + public String toString() { + return "CancelExternalWorkflowException{" + + "execution=" + + getExecution() + + ", workflowType='" + + getWorkflowType() + + '\'' + + '}'; } } diff --git a/src/main/java/io/temporal/workflow/CancellationScope.java b/src/main/java/io/temporal/workflow/CancellationScope.java index edcd442c89..078ab56e4e 100644 --- a/src/main/java/io/temporal/workflow/CancellationScope.java +++ b/src/main/java/io/temporal/workflow/CancellationScope.java @@ -19,8 +19,8 @@ package io.temporal.workflow; +import io.temporal.failure.CanceledFailure; import io.temporal.internal.sync.WorkflowInternal; -import java.util.concurrent.CancellationException; /** * Handle to a cancellation scope created through {@link Workflow#newCancellationScope(Runnable)} or @@ -43,7 +43,7 @@ public interface CancellationScope extends Runnable { * Cancels the scope as well as all its children. * * @param reason human readable reason for the cancellation. Becomes message of the - * CancellationException thrown. + * CanceledException thrown. */ void cancel(String reason); @@ -68,13 +68,10 @@ static CancellationScope current() { return WorkflowInternal.currentCancellationScope(); } - /** - * Throws {@link java.util.concurrent.CancellationException} if scope is cancelled. Noop if not - * cancelled. - */ - static void throwCancelled() throws CancellationException { + /** Throws {@link CanceledFailure} if scope is cancelled. Noop if not cancelled. */ + static void throwCancelled() throws CanceledFailure { if (current().isCancelRequested()) { - throw new CancellationException(); + throw new CanceledFailure(current().getCancellationReason()); } } } diff --git a/src/main/java/io/temporal/workflow/ChildWorkflowCancellationType.java b/src/main/java/io/temporal/workflow/ChildWorkflowCancellationType.java index b327809b92..064ed7bd49 100644 --- a/src/main/java/io/temporal/workflow/ChildWorkflowCancellationType.java +++ b/src/main/java/io/temporal/workflow/ChildWorkflowCancellationType.java @@ -19,13 +19,13 @@ package io.temporal.workflow; +import io.temporal.failure.CanceledFailure; import io.temporal.proto.common.ParentClosePolicy; -import java.util.concurrent.CancellationException; /** * Defines behaviour of the parent workflow when {@link CancellationScope} that wraps child workflow * execution request is cancelled. The result of the cancellation independently of the type is a - * {@link CancellationException} thrown from the child workflow method. + * {@link CanceledFailure} thrown from the child workflow method. */ public enum ChildWorkflowCancellationType { /** Wait for child cancellation completion. */ diff --git a/src/main/java/io/temporal/workflow/ChildWorkflowException.java b/src/main/java/io/temporal/workflow/ChildWorkflowException.java deleted file mode 100644 index 1259d827d7..0000000000 --- a/src/main/java/io/temporal/workflow/ChildWorkflowException.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. - * - * 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 io.temporal.workflow; - -import io.temporal.proto.common.WorkflowExecution; -import io.temporal.proto.common.WorkflowType; - -/** Base exception for failures of a child workflow. */ -@SuppressWarnings("serial") -public abstract class ChildWorkflowException extends WorkflowOperationException { - - private final WorkflowExecution workflowExecution; - - private final WorkflowType workflowType; - - protected ChildWorkflowException( - String message, - long eventId, - WorkflowExecution workflowExecution, - WorkflowType workflowType) { - super( - message - + " WorkflowType=\"" - + workflowType.getName() - + "\", ID=\"" - + workflowExecution.getWorkflowId() - + "\", RunId=\"" - + workflowExecution.getRunId() - + ", EventId=" - + eventId, - eventId); - - this.workflowExecution = workflowExecution; - this.workflowType = workflowType; - } - - public WorkflowExecution getWorkflowExecution() { - return workflowExecution; - } - - public WorkflowType getWorkflowType() { - return workflowType; - } -} diff --git a/src/main/java/io/temporal/workflow/ChildWorkflowOptions.java b/src/main/java/io/temporal/workflow/ChildWorkflowOptions.java index 41ff532031..a1e0537d0a 100644 --- a/src/main/java/io/temporal/workflow/ChildWorkflowOptions.java +++ b/src/main/java/io/temporal/workflow/ChildWorkflowOptions.java @@ -26,13 +26,13 @@ import io.temporal.common.MethodRetry; import io.temporal.common.RetryOptions; import io.temporal.common.context.ContextPropagator; +import io.temporal.failure.CanceledFailure; import io.temporal.internal.common.OptionsUtils; import io.temporal.proto.common.ParentClosePolicy; import io.temporal.proto.common.WorkflowIdReusePolicy; import java.time.Duration; import java.util.List; import java.util.Map; -import java.util.concurrent.CancellationException; public final class ChildWorkflowOptions { @@ -231,8 +231,8 @@ public Builder setContextPropagators(List contextPropagators) } /** - * In case of a child workflow cancellation it fails with a {@link CancellationException}. The - * type defines at which point the exception is thrown. + * In case of a child workflow cancellation it fails with a {@link CanceledFailure}. The type + * defines at which point the exception is thrown. */ public Builder setCancellationType(ChildWorkflowCancellationType cancellationType) { this.cancellationType = cancellationType; diff --git a/src/main/java/io/temporal/workflow/Promise.java b/src/main/java/io/temporal/workflow/Promise.java index 523a79cbae..6807a6fcbf 100644 --- a/src/main/java/io/temporal/workflow/Promise.java +++ b/src/main/java/io/temporal/workflow/Promise.java @@ -19,6 +19,7 @@ package io.temporal.workflow; +import io.temporal.failure.CanceledFailure; import io.temporal.internal.sync.WorkflowInternal; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -38,7 +39,7 @@ * exceptions. So wrapping must be done by the caller of that method. *

  • Promise doesn't directly supports cancellation. Use {@link CancellationScope} to cancel and * handle cancellations. The pattern is that a cancelled operation completes its Promise with - * {@link java.util.concurrent.CancellationException} when cancelled. + * {@link CanceledFailure} when cancelled. *
  • {@link #handle(Functions.Func2)} and similar callback operations do not allow blocking * calls inside functions * @@ -65,16 +66,14 @@ public interface Promise { /** * Waits if necessary for the computation to complete or fail, and then returns its result. This - * call is going to throw {@link java.util.concurrent.CancellationException} without waiting for - * this Promise to become ready. Note that in the most situations it is better to let the - * operation that returned a Promise to perform cleanup and then complete the promise with - * CancellationException. So calling {@link #get()} on an asynchronous activity or child workflow - * invocation result is preferable. + * call is going to throw {@link CanceledFailure} without waiting for this Promise to become + * ready. Note that in the most situations it is better to let the operation that returned a + * Promise to perform cleanup and then complete the promise with CanceledException. So calling + * {@link #get()} on an asynchronous activity or child workflow invocation result is preferable. * * @return the computed result * @throws RuntimeException if the computation failed. - * @throws java.util.concurrent.CancellationException if surrounding @{@link CancellationScope} is - * cancelled. + * @throws CanceledFailure if surrounding @{@link CancellationScope} is cancelled. */ V cancellableGet(); @@ -92,20 +91,18 @@ public interface Promise { /** * Waits if necessary for at most the given time for the computation to complete, and then returns - * its result, if available. This call is going to throw {@link - * java.util.concurrent.CancellationException} without waiting for this Promise to become ready. - * Note that in the most situations it is better to let the operation that returned a Promise to - * perform cleanup and then complete the promise with CancellationException. So calling {@link - * #get(long, TimeUnit)} on an asynchronous activity or child workflow invocation result is - * preferable. + * its result, if available. This call is going to throw {@link CanceledFailure} without waiting + * for this Promise to become ready. Note that in the most situations it is better to let the + * operation that returned a Promise to perform cleanup and then complete the promise with + * CanceledException. So calling {@link #get(long, TimeUnit)} on an asynchronous activity or child + * workflow invocation result is preferable. * * @param timeout the maximum time to wait * @param unit the time unit of the timeout argument * @return the computed result * @throws RuntimeException if the computation failed. * @throws TimeoutException if the wait timed out - * @throws java.util.concurrent.CancellationException if surrounding @{@link CancellationScope} is - * cancelled. + * @throws CanceledFailure if surrounding @{@link CancellationScope} is cancelled. */ V cancellableGet(long timeout, TimeUnit unit) throws TimeoutException; diff --git a/src/main/java/io/temporal/workflow/QueueConsumer.java b/src/main/java/io/temporal/workflow/QueueConsumer.java index 0f9c8ff6b6..8b130fa576 100644 --- a/src/main/java/io/temporal/workflow/QueueConsumer.java +++ b/src/main/java/io/temporal/workflow/QueueConsumer.java @@ -37,7 +37,7 @@ public interface QueueConsumer { * available. * * @return the head of this queue - * @throws java.util.concurrent.CancellationException if surrounding @{@link CancellationScope} is + * @throws io.temporal.failure.CanceledFailure if surrounding @{@link CancellationScope} is * cancelled while waiting */ E cancellableTake(); @@ -78,7 +78,7 @@ public interface QueueConsumer { * @param unit a {@code TimeUnit} determining how to interpret the {@code timeout} parameter * @return the head of this queue, or {@code null} if the specified waiting time elapses before an * element is available - * @throws java.util.concurrent.CancellationException if surrounding @{@link CancellationScope} is + * @throws io.temporal.failure.CanceledFailure if surrounding @{@link CancellationScope} is * cancelled while waiting */ E cancellablePoll(long timeout, TimeUnit unit); diff --git a/src/main/java/io/temporal/workflow/QueueProducer.java b/src/main/java/io/temporal/workflow/QueueProducer.java index 2035248db4..c0a209a2ae 100644 --- a/src/main/java/io/temporal/workflow/QueueProducer.java +++ b/src/main/java/io/temporal/workflow/QueueProducer.java @@ -57,7 +57,7 @@ public interface QueueProducer { * available. * * @param e the element to add - * @throws java.util.concurrent.CancellationException if surrounding @{@link CancellationScope} is + * @throws io.temporal.failure.CanceledFailure if surrounding @{@link CancellationScope} is * cancelled while waiting * @throws ClassCastException if the class of the specified element prevents it from being added * to this queue @@ -94,7 +94,7 @@ public interface QueueProducer { * @param unit a {@code TimeUnit} determining how to interpret the {@code timeout} parameter * @return {@code true} if successful, or {@code false} if the specified waiting time elapses * before space is available - * @throws java.util.concurrent.CancellationException if surrounding @{@link CancellationScope} is + * @throws io.temporal.failure.CanceledFailure if surrounding @{@link CancellationScope} is * cancelled while waiting * @throws ClassCastException if the class of the specified element prevents it from being added * to this queue diff --git a/src/main/java/io/temporal/workflow/SignalExternalWorkflowException.java b/src/main/java/io/temporal/workflow/SignalExternalWorkflowException.java index a4a961b150..89680aa50e 100644 --- a/src/main/java/io/temporal/workflow/SignalExternalWorkflowException.java +++ b/src/main/java/io/temporal/workflow/SignalExternalWorkflowException.java @@ -19,29 +19,23 @@ package io.temporal.workflow; +import io.temporal.client.WorkflowException; import io.temporal.proto.common.WorkflowExecution; -import io.temporal.proto.event.WorkflowExecutionFailedCause; /** Exception used to communicate failure of a request to signal an external workflow. */ @SuppressWarnings("serial") -public final class SignalExternalWorkflowException extends WorkflowOperationException { +public final class SignalExternalWorkflowException extends WorkflowException { - private WorkflowExecutionFailedCause failureCause; - - private WorkflowExecution signaledExecution; - - public SignalExternalWorkflowException( - long eventId, WorkflowExecution signaledExecution, WorkflowExecutionFailedCause cause) { - super(cause + " for signaledExecution=\"" + signaledExecution, eventId); - this.signaledExecution = signaledExecution; - this.failureCause = cause; - } - - public WorkflowExecutionFailedCause getFailureCause() { - return failureCause; + public SignalExternalWorkflowException(WorkflowExecution execution, String workflowType) { + super(getMessage(execution, workflowType), execution, workflowType, null); } - public WorkflowExecution getSignaledExecution() { - return signaledExecution; + public static String getMessage(WorkflowExecution execution, String workflowType) { + return "message='Open execution not found', workflowId='" + + execution.getWorkflowId() + + "', runId='" + + execution.getRunId() + + "'" + + (workflowType == null ? "" : "', workflowType='" + workflowType + '\''); } } diff --git a/src/main/java/io/temporal/workflow/StartChildWorkflowFailedException.java b/src/main/java/io/temporal/workflow/StartChildWorkflowFailedException.java deleted file mode 100644 index f5a389ecee..0000000000 --- a/src/main/java/io/temporal/workflow/StartChildWorkflowFailedException.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. - * - * 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 io.temporal.workflow; - -import io.temporal.proto.common.WorkflowExecution; -import io.temporal.proto.common.WorkflowType; -import io.temporal.proto.event.WorkflowExecutionFailedCause; - -/** - * Indicates that child workflow failed to start. Currently the only cause is that there is already - * workflow running with the same ID. - */ -@SuppressWarnings("serial") -public final class StartChildWorkflowFailedException extends ChildWorkflowException { - - private WorkflowExecutionFailedCause failureCause; - - public StartChildWorkflowFailedException( - long eventId, - WorkflowExecution workflowExecution, - WorkflowType workflowType, - WorkflowExecutionFailedCause cause) { - super(String.valueOf(cause), eventId, workflowExecution, workflowType); - this.failureCause = cause; - } - - /** @return enumeration that contains the cause of the failure */ - public WorkflowExecutionFailedCause getFailureCause() { - return failureCause; - } -} diff --git a/src/main/java/io/temporal/workflow/Workflow.java b/src/main/java/io/temporal/workflow/Workflow.java index 8dc6d51892..e576607757 100644 --- a/src/main/java/io/temporal/workflow/Workflow.java +++ b/src/main/java/io/temporal/workflow/Workflow.java @@ -23,6 +23,9 @@ import io.temporal.activity.ActivityOptions; import io.temporal.activity.LocalActivityOptions; import io.temporal.common.RetryOptions; +import io.temporal.failure.ActivityFailure; +import io.temporal.failure.CanceledFailure; +import io.temporal.failure.ChildWorkflowFailure; import io.temporal.internal.sync.WorkflowInternal; import io.temporal.proto.common.WorkflowExecution; import io.temporal.worker.WorkerOptions; @@ -33,7 +36,6 @@ import java.util.Optional; import java.util.Random; import java.util.UUID; -import java.util.concurrent.CancellationException; import java.util.function.BiPredicate; import java.util.function.Supplier; import org.slf4j.Logger; @@ -580,9 +582,9 @@ public static WorkflowInfo getWorkflowInfo() { * CancellationScope#run()} calls {@link Runnable#run()} on the wrapped Runnable. The returned * CancellationScope can be used to cancel the wrapped code. The cancellation semantic depends on * the operation the code is blocked on. For example activity or child workflow is first cancelled - * then throws a {@link CancellationException}. The same applies for {@link Workflow#sleep(long)} + * then throws a {@link CanceledFailure}. The same applies for {@link Workflow#sleep(long)} * operation. When an activity or a child workflow is invoked asynchronously then they get - * cancelled and a {@link Promise} that contains their result will throw CancellationException + * cancelled and a {@link Promise} that contains their result will throw {@link CanceledFailure} * when {@link Promise#get()} is called. * *

    The new cancellation scope is linked to the parent one (available as {@link @@ -655,7 +657,7 @@ public static CancellationScope newCancellationScope(Functions.Proc1 * try { * // workflow logic - * } catch (CancellationException e) { + * } catch (CanceledFailure e) { * CancellationScope detached = Workflow.newDetachedCancellationScope(() -> { * // cleanup logic * }); @@ -676,8 +678,7 @@ public static CancellationScope newDetachedCancellationScope(Runnable runnable) * are rounded up to the nearest second. * * @return feature that becomes ready when at least specified number of seconds passes. promise is - * failed with {@link java.util.concurrent.CancellationException} if enclosing scope is - * cancelled. + * failed with {@link CanceledFailure} if enclosing scope is cancelled. */ public static Promise newTimer(Duration delay) { return WorkflowInternal.newTimer(delay); @@ -737,7 +738,7 @@ public static void sleep(long millis) { * * @param unblockCondition condition that should return true to indicate that thread should * unblock. - * @throws CancellationException if thread (or current {@link CancellationScope} was cancelled). + * @throws CanceledFailure if thread (or current {@link CancellationScope} was cancelled). */ public static void await(Supplier unblockCondition) { WorkflowInternal.await( @@ -753,7 +754,7 @@ public static void await(Supplier unblockCondition) { * passes. * * @return false if timed out. - * @throws CancellationException if thread (or current {@link CancellationScope} was cancelled). + * @throws CanceledFailure if thread (or current {@link CancellationScope} was cancelled). */ public static boolean await(Duration timeout, Supplier unblockCondition) { return WorkflowInternal.await( @@ -767,7 +768,7 @@ public static boolean await(Duration timeout, Supplier unblockCondition /** * Invokes function retrying in case of failures according to retry options. Synchronous variant. - * Use {@link Async#retry(RetryOptions, Optional, Func)} for asynchronous functions. + * Use {@link Async#retry(RetryOptions, Optional, Functions.Func)} for asynchronous functions. * * @param options retry options that specify retry policy * @param expiration stop retrying after this interval if provided @@ -781,7 +782,8 @@ public static R retry( /** * Invokes function retrying in case of failures according to retry options. Synchronous variant. - * Use {@link Async#retry(RetryOptions, Optional, Func)} for asynchronous functions. + * Use {@link Async#retry(RetryOptions, Optional, Functions.Func)} (RetryOptions, Optional, Func)} + * for asynchronous functions. * * @param options retry options that specify retry policy * @param expiration if specified stop retrying after this interval @@ -808,10 +810,10 @@ public static void retry( *

    The reason for such design is that returning originally thrown exception from a remote call * (which child workflow and activity invocations are ) would not allow adding context information * about a failure, like activity and child workflow id. So stubs always throw a subclass of - * {@link ActivityException} from calls to an activity and subclass of {@link - * io.temporal.workflow.ChildWorkflowException} from calls to a child workflow. The original - * exception is attached as a cause to these wrapper exceptions. So as exceptions are always - * wrapped adding checked ones to method signature causes more pain than benefit. + * {@link ActivityFailure} from calls to an activity and subclass of {@link ChildWorkflowFailure} + * from calls to a child workflow. The original exception is attached as a cause to these wrapper + * exceptions. So as exceptions are always wrapped adding checked ones to method signature causes + * more pain than benefit. * *

    * diff --git a/src/main/proto b/src/main/proto index 8f34c27c6d..85c74fd187 160000 --- a/src/main/proto +++ b/src/main/proto @@ -1 +1 @@ -Subproject commit 8f34c27c6dab81d2e04fce3bcd5845609384771d +Subproject commit 85c74fd187cf18a705c4b4ca3b6a49e63df48af1 diff --git a/src/test/java/io/temporal/activity/ActivityOptionsTest.java b/src/test/java/io/temporal/activity/ActivityOptionsTest.java index 1d4ef40f68..9819f93179 100644 --- a/src/test/java/io/temporal/activity/ActivityOptionsTest.java +++ b/src/test/java/io/temporal/activity/ActivityOptionsTest.java @@ -23,7 +23,6 @@ import io.temporal.common.RetryOptions; import java.lang.reflect.Method; import java.time.Duration; -import java.util.Arrays; import org.junit.Assert; import org.junit.Test; @@ -34,7 +33,7 @@ public class ActivityOptionsTest { backoffCoefficient = 1.97, maximumAttempts = 234567, maximumIntervalSeconds = 22, - doNotRetry = {NullPointerException.class, UnsupportedOperationException.class}) + doNotRetry = {"java.lang.NullPointerException", "java.lang.UnsupportedOperationException"}) public void activityAndRetryOptions() {} @Test @@ -52,6 +51,6 @@ public void testOnlyAnnotationsPresent() throws NoSuchMethodException { Duration.ofSeconds(r.initialIntervalSeconds()), rMerged.getInitialInterval()); Assert.assertEquals( Duration.ofSeconds(r.maximumIntervalSeconds()), rMerged.getMaximumInterval()); - Assert.assertEquals(Arrays.asList(r.doNotRetry()), rMerged.getDoNotRetry()); + Assert.assertArrayEquals(r.doNotRetry(), rMerged.getDoNotRetry()); } } diff --git a/src/test/java/io/temporal/client/WorkflowOptionsTest.java b/src/test/java/io/temporal/client/WorkflowOptionsTest.java index 32c634062b..79be08c67b 100644 --- a/src/test/java/io/temporal/client/WorkflowOptionsTest.java +++ b/src/test/java/io/temporal/client/WorkflowOptionsTest.java @@ -58,7 +58,7 @@ public void testOnlyOptionsAndEmptyAnnotationsPresent() throws NoSuchMethodExcep backoffCoefficient = 1.97, maximumAttempts = 234567, maximumIntervalSeconds = 22, - doNotRetry = {NullPointerException.class, UnsupportedOperationException.class}) + doNotRetry = {"java.lang.NullPointerException", "java.lang.UnsupportedOperationException"}) @CronSchedule("0 * * * *" /* hourly */) public void workflowOptions() {} @@ -76,7 +76,7 @@ public void testOnlyAnnotationsPresent() throws NoSuchMethodException { public void testBothPresent() throws NoSuchMethodException { RetryOptions retryOptions = RetryOptions.newBuilder() - .setDoNotRetry(IllegalArgumentException.class) + .setDoNotRetry(IllegalArgumentException.class.getName()) .setMaximumAttempts(11111) .setBackoffCoefficient(1.55) .setMaximumInterval(Duration.ofDays(3)) @@ -112,7 +112,7 @@ public void testBothPresent() throws NoSuchMethodException { public void testChildWorkflowOptionMerge() throws NoSuchMethodException { RetryOptions retryOptions = RetryOptions.newBuilder() - .setDoNotRetry(IllegalArgumentException.class) + .setDoNotRetry(IllegalArgumentException.class.getName()) .setMaximumAttempts(11111) .setBackoffCoefficient(1.55) .setMaximumInterval(Duration.ofDays(3)) diff --git a/src/test/java/io/temporal/common/converter/EncodedValueTest.java b/src/test/java/io/temporal/common/converter/EncodedValueTest.java new file mode 100644 index 0000000000..0dc0ee3821 --- /dev/null +++ b/src/test/java/io/temporal/common/converter/EncodedValueTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2020 Temporal Technologies, Inc. All Rights Reserved. + * + * 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 io.temporal.common.converter; + +import static org.junit.Assert.assertEquals; + +import com.google.common.base.Objects; +import com.google.common.reflect.TypeToken; +import io.temporal.proto.common.Payloads; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.junit.Test; + +public class EncodedValueTest { + + public static class Pair { + public int i; + public String s; + + public Pair(int i, String s) { + this.i = i; + this.s = s; + } + + public Pair() {} + + public int getI() { + return i; + } + + public String getS() { + return s; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Pair pair = (Pair) o; + return i == pair.i && Objects.equal(s, pair.s); + } + + @Override + public int hashCode() { + return Objects.hashCode(i, s); + } + } + + @Test + @SuppressWarnings("uncheckedkk") + public void testGenericParameter() { + ArrayList list = new ArrayList<>(); + list.add(new Pair(10, "foo")); + list.add(new Pair(12, "bar")); + EncodedValue v = new EncodedValue(list); + DataConverter converter = DefaultDataConverter.getDefaultInstance(); + v.setDataConverter(converter); + Optional payloads = v.toPayloads(); + Value v2 = new EncodedValue(payloads, converter); + TypeToken> typeToken = new TypeToken>() {}; + List result = v2.get(List.class, typeToken.getType()); + assertEquals(list, result); + } +} diff --git a/src/test/java/io/temporal/common/converter/JsonDataConverterTest.java b/src/test/java/io/temporal/common/converter/JsonDataConverterTest.java index 73874356b1..e95f3af058 100644 --- a/src/test/java/io/temporal/common/converter/JsonDataConverterTest.java +++ b/src/test/java/io/temporal/common/converter/JsonDataConverterTest.java @@ -20,15 +20,9 @@ package io.temporal.common.converter; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import com.google.common.base.Objects; -import io.temporal.activity.Activity; import io.temporal.proto.common.Payloads; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; @@ -39,7 +33,7 @@ public class JsonDataConverterTest { - private final DataConverter converter = GsonJsonDataConverter.getInstance(); + private final DataConverter converter = DataConverter.getDefaultInstance(); public static void foo(List arg) {} @@ -54,9 +48,10 @@ public void testUUIDList() throws NoSuchMethodException { list.add(UUID.randomUUID()); } - Optional data = converter.toData(list); + Optional data = converter.toPayloads(list); @SuppressWarnings("unchecked") - List result = (List) converter.fromData(data, parameterType, genericParameterType); + List result = + (List) converter.fromPayloads(data, parameterType, genericParameterType); assertEquals(result.toString(), list, result); } @@ -109,9 +104,9 @@ public void AdditionalInputArgumentsAreIgnored() throws NoSuchMethodException { list.add(new Struct1(234, "s1")); list.add(new Struct1(567, "s2")); Optional data = - converter.toData(1234, struct1, "a string", list, "an extra string :o!!!"); + converter.toPayloads(1234, struct1, "a string", list, "an extra string :o!!!"); Object[] deserializedArguments = - converter.fromDataArray(data, m.getParameterTypes(), m.getGenericParameterTypes()); + converter.arrayFromPayloads(data, m.getParameterTypes(), m.getGenericParameterTypes()); assertEquals(4, deserializedArguments.length); assertEquals(1234, (int) deserializedArguments[0]); assertEquals(struct1, deserializedArguments[1]); @@ -126,10 +121,10 @@ public void MissingInputArgumentsArePopulatedWithDefaultValues() throws NoSuchMe Method m = JsonDataConverterTest.class.getDeclaredMethod( "aLotOfArguments", int.class, Struct1.class, String.class, Object.class, int[].class); - Optional data = converter.toData(1); + Optional data = converter.toPayloads(1); @SuppressWarnings("unchecked") Object[] deserializedArguments = - converter.fromDataArray(data, m.getParameterTypes(), m.getGenericParameterTypes()); + converter.arrayFromPayloads(data, m.getParameterTypes(), m.getGenericParameterTypes()); assertEquals(5, deserializedArguments.length); assertEquals(1, (int) deserializedArguments[0]); assertEquals(null, deserializedArguments[1]); @@ -137,54 +132,4 @@ public void MissingInputArgumentsArePopulatedWithDefaultValues() throws NoSuchMe assertEquals(null, deserializedArguments[3]); assertEquals(null, deserializedArguments[4]); } - - @Test - public void testClass() { - - Optional data = converter.toData(this.getClass()); - @SuppressWarnings("unchecked") - Class result = converter.fromData(data, Class.class, Class.class); - assertEquals(result.toString(), this.getClass(), result); - } - - public static class NonSerializableException extends RuntimeException { - @SuppressWarnings("unused") - private final InputStream file; // gson chokes on this field - - private final String foo; - - public NonSerializableException(Throwable cause) { - super(cause); - try { - file = new FileInputStream(File.createTempFile("foo", "bar")); - } catch (IOException e) { - throw Activity.wrap(e); - } - foo = "bar"; - } - } - - @Test - public void testException() { - RuntimeException rootException = new IllegalArgumentException("root exception"); - NonSerializableException nonSerializableCause = new NonSerializableException(rootException); - RuntimeException e = new RuntimeException("application exception", nonSerializableCause); - - Optional converted = converter.toData(e); - RuntimeException fromConverted = - converter.fromData(converted, RuntimeException.class, RuntimeException.class); - assertEquals(RuntimeException.class, fromConverted.getClass()); - assertEquals("application exception", fromConverted.getMessage()); - - Throwable causeFromConverted = fromConverted.getCause(); - assertNotNull(causeFromConverted); - assertEquals(DataConverterException.class, causeFromConverted.getClass()); - assertNotNull(causeFromConverted.getCause()); - assertEquals(StackOverflowError.class, causeFromConverted.getCause().getClass()); - - assertNotNull(causeFromConverted.getSuppressed()); - assertEquals(1, causeFromConverted.getSuppressed().length); - - assertEquals("root exception", causeFromConverted.getSuppressed()[0].getMessage()); - } } diff --git a/src/test/java/io/temporal/internal/common/InternalUtilsTest.java b/src/test/java/io/temporal/internal/common/InternalUtilsTest.java index 86bb4758a6..b52704d712 100644 --- a/src/test/java/io/temporal/internal/common/InternalUtilsTest.java +++ b/src/test/java/io/temporal/internal/common/InternalUtilsTest.java @@ -21,10 +21,9 @@ import static junit.framework.TestCase.assertEquals; +import io.temporal.common.converter.DataConverter; import io.temporal.common.converter.DataConverterException; -import io.temporal.common.converter.GsonJsonDataConverter; import io.temporal.proto.common.SearchAttributes; -import io.temporal.workflow.WorkflowUtils; import java.io.FileOutputStream; import java.util.HashMap; import java.util.Map; @@ -39,18 +38,17 @@ public void testConvertMapToSearchAttributes() throws Throwable { attr.put("CustomKeywordField", value); SearchAttributes result = - InternalUtils.convertMapToSearchAttributes( - attr, GsonJsonDataConverter.getInstance().getPayloadConverter()); + InternalUtils.convertMapToSearchAttributes(attr, DataConverter.getDefaultInstance()); assertEquals( value, - WorkflowUtils.getValueFromSearchAttributes(result, "CustomKeywordField", String.class)); + SearchAttributesUtil.getValueFromSearchAttributes( + result, "CustomKeywordField", String.class)); } @Test(expected = DataConverterException.class) public void testConvertMapToSearchAttributesException() throws Throwable { Map attr = new HashMap<>(); attr.put("InvalidValue", new FileOutputStream("dummy")); - InternalUtils.convertMapToSearchAttributes( - attr, GsonJsonDataConverter.getInstance().getPayloadConverter()); + InternalUtils.convertMapToSearchAttributes(attr, DataConverter.getDefaultInstance()); } } diff --git a/src/test/java/io/temporal/internal/common/RetryerTest.java b/src/test/java/io/temporal/internal/common/RetryerTest.java index 47530b5088..1c262e0dc6 100644 --- a/src/test/java/io/temporal/internal/common/RetryerTest.java +++ b/src/test/java/io/temporal/internal/common/RetryerTest.java @@ -88,7 +88,7 @@ public void testInterruptedException() throws InterruptedException { RetryOptions.newBuilder() .setInitialInterval(Duration.ofMillis(10)) .setMaximumInterval(Duration.ofMillis(100)) - .setDoNotRetry(InterruptedException.class) + .setDoNotRetry(InterruptedException.class.getName()) .validateBuildWithDefaults(); long start = System.currentTimeMillis(); try { diff --git a/src/main/java/io/temporal/workflow/WorkflowUtils.java b/src/test/java/io/temporal/internal/common/SearchAttributesUtil.java similarity index 75% rename from src/main/java/io/temporal/workflow/WorkflowUtils.java rename to src/test/java/io/temporal/internal/common/SearchAttributesUtil.java index 11fb2f723c..9b1fbbceb8 100644 --- a/src/main/java/io/temporal/workflow/WorkflowUtils.java +++ b/src/test/java/io/temporal/internal/common/SearchAttributesUtil.java @@ -17,23 +17,21 @@ * permissions and limitations under the License. */ -package io.temporal.workflow; +package io.temporal.internal.common; import com.cronutils.utils.StringUtils; import io.temporal.common.converter.DataConverter; -import io.temporal.common.converter.GsonJsonDataConverter; import io.temporal.proto.common.SearchAttributes; -public class WorkflowUtils { - private static final DataConverter jsonConverter = GsonJsonDataConverter.getInstance(); +public class SearchAttributesUtil { + private static final DataConverter jsonConverter = DataConverter.getDefaultInstance(); public static T getValueFromSearchAttributes( SearchAttributes searchAttributes, String key, Class classType) { if (searchAttributes == null || StringUtils.isEmpty(key)) { return null; } - return jsonConverter - .getPayloadConverter() - .fromData(searchAttributes.getIndexedFieldsOrThrow(key), classType, classType); + return jsonConverter.fromPayload( + searchAttributes.getIndexedFieldsOrThrow(key), classType, classType); } } diff --git a/src/test/java/io/temporal/workflow/WorkflowUtilsTest.java b/src/test/java/io/temporal/internal/common/SearchAttributesUtilTest.java similarity index 72% rename from src/test/java/io/temporal/workflow/WorkflowUtilsTest.java rename to src/test/java/io/temporal/internal/common/SearchAttributesUtilTest.java index 7049e9d03c..ceb9ac9534 100644 --- a/src/test/java/io/temporal/workflow/WorkflowUtilsTest.java +++ b/src/test/java/io/temporal/internal/common/SearchAttributesUtilTest.java @@ -17,25 +17,25 @@ * permissions and limitations under the License. */ -package io.temporal.workflow; +package io.temporal.internal.common; import static junit.framework.TestCase.assertEquals; -import io.temporal.common.converter.GsonJsonDataConverter; -import io.temporal.internal.common.InternalUtils; +import io.temporal.common.converter.DataConverter; import io.temporal.proto.common.SearchAttributes; import java.util.HashMap; import java.util.Map; import org.junit.Test; -public class WorkflowUtilsTest { +public class SearchAttributesUtilTest { @Test public void TestGetValueFromSearchAttributes() { - assertEquals(null, WorkflowUtils.getValueFromSearchAttributes(null, "key", String.class)); + assertEquals( + null, SearchAttributesUtil.getValueFromSearchAttributes(null, "key", String.class)); assertEquals( null, - WorkflowUtils.getValueFromSearchAttributes( + SearchAttributesUtil.getValueFromSearchAttributes( SearchAttributes.getDefaultInstance(), "", String.class)); Map attr = new HashMap<>(); @@ -48,24 +48,23 @@ public void TestGetValueFromSearchAttributes() { Boolean boolVal = Boolean.TRUE; attr.put("CustomBooleanField", boolVal); SearchAttributes searchAttributes = - InternalUtils.convertMapToSearchAttributes( - attr, GsonJsonDataConverter.getInstance().getPayloadConverter()); + InternalUtils.convertMapToSearchAttributes(attr, DataConverter.getDefaultInstance()); assertEquals( stringVal, - WorkflowUtils.getValueFromSearchAttributes( + SearchAttributesUtil.getValueFromSearchAttributes( searchAttributes, "CustomKeywordField", String.class)); assertEquals( intVal, - WorkflowUtils.getValueFromSearchAttributes( + SearchAttributesUtil.getValueFromSearchAttributes( searchAttributes, "CustomIntField", Integer.class)); assertEquals( doubleVal, - WorkflowUtils.getValueFromSearchAttributes( + SearchAttributesUtil.getValueFromSearchAttributes( searchAttributes, "CustomDoubleField", Double.class)); assertEquals( boolVal, - WorkflowUtils.getValueFromSearchAttributes( + SearchAttributesUtil.getValueFromSearchAttributes( searchAttributes, "CustomBooleanField", Boolean.class)); } @@ -75,15 +74,14 @@ public void TestGetValueFromSearchAttributesRepeated() { String stringVal = "keyword"; attr.put("CustomKeywordField", stringVal); SearchAttributes searchAttributes = - InternalUtils.convertMapToSearchAttributes( - attr, GsonJsonDataConverter.getInstance().getPayloadConverter()); + InternalUtils.convertMapToSearchAttributes(attr, DataConverter.getDefaultInstance()); assertEquals( stringVal, - WorkflowUtils.getValueFromSearchAttributes( + SearchAttributesUtil.getValueFromSearchAttributes( searchAttributes, "CustomKeywordField", String.class)); assertEquals( stringVal, - WorkflowUtils.getValueFromSearchAttributes( + SearchAttributesUtil.getValueFromSearchAttributes( searchAttributes, "CustomKeywordField", String.class)); } } diff --git a/src/test/java/io/temporal/internal/replay/ReplayDeciderCacheTests.java b/src/test/java/io/temporal/internal/replay/ReplayDeciderCacheTests.java index ea0cea84e5..c062b7e9b7 100644 --- a/src/test/java/io/temporal/internal/replay/ReplayDeciderCacheTests.java +++ b/src/test/java/io/temporal/internal/replay/ReplayDeciderCacheTests.java @@ -309,7 +309,7 @@ public Optional query(WorkflowQuery query) { } @Override - public WorkflowExecutionException mapUnexpectedException(Exception failure) { + public WorkflowExecutionException mapUnexpectedException(Throwable failure) { return null; } diff --git a/src/test/java/io/temporal/internal/replay/WorkflowContextTest.java b/src/test/java/io/temporal/internal/replay/WorkflowContextTest.java index 3c6988fcb2..6c9cd4cca9 100644 --- a/src/test/java/io/temporal/internal/replay/WorkflowContextTest.java +++ b/src/test/java/io/temporal/internal/replay/WorkflowContextTest.java @@ -21,12 +21,11 @@ import static junit.framework.TestCase.assertEquals; -import io.temporal.common.converter.GsonJsonDataConverter; -import io.temporal.common.converter.PayloadConverter; +import io.temporal.common.converter.DataConverter; +import io.temporal.internal.common.SearchAttributesUtil; import io.temporal.proto.common.Payload; import io.temporal.proto.common.SearchAttributes; import io.temporal.proto.event.WorkflowExecutionStartedEventAttributes; -import io.temporal.workflow.WorkflowUtils; import java.util.HashMap; import java.util.Map; import org.junit.Test; @@ -39,9 +38,9 @@ public void TestMergeSearchAttributes() { WorkflowExecutionStartedEventAttributes.getDefaultInstance(); WorkflowContext workflowContext = new WorkflowContext("namespace", null, startAttr, 0, null); - PayloadConverter converter = GsonJsonDataConverter.getInstance().getPayloadConverter(); + DataConverter converter = DataConverter.getDefaultInstance(); Map indexedFields = new HashMap<>(); - indexedFields.put("CustomKeywordField", converter.toData("key").get()); + indexedFields.put("CustomKeywordField", converter.toPayload("key").get()); SearchAttributes searchAttributes = SearchAttributes.newBuilder().putAllIndexedFields(indexedFields).build(); @@ -50,7 +49,7 @@ public void TestMergeSearchAttributes() { assertEquals( "key", - WorkflowUtils.getValueFromSearchAttributes( + SearchAttributesUtil.getValueFromSearchAttributes( workflowContext.getSearchAttributes(), "CustomKeywordField", String.class)); } } diff --git a/src/test/java/io/temporal/internal/sync/DeterministicRunnerTest.java b/src/test/java/io/temporal/internal/sync/DeterministicRunnerTest.java index 5fa279be82..59832f4fa7 100644 --- a/src/test/java/io/temporal/internal/sync/DeterministicRunnerTest.java +++ b/src/test/java/io/temporal/internal/sync/DeterministicRunnerTest.java @@ -29,7 +29,8 @@ import com.uber.m3.tally.StatsReporter; import com.uber.m3.util.ImmutableMap; import io.temporal.common.RetryOptions; -import io.temporal.common.converter.GsonJsonDataConverter; +import io.temporal.common.converter.DataConverter; +import io.temporal.failure.CanceledFailure; import io.temporal.internal.metrics.MetricsTag; import io.temporal.internal.metrics.MetricsType; import io.temporal.internal.metrics.NoopScope; @@ -55,7 +56,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutorService; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -353,7 +353,7 @@ public void testExplicitScopeCancellation() throws Throwable { try { var.get(); trace.add("after get"); - } catch (CancellationException e) { + } catch (CanceledFailure e) { trace.add("scope cancelled"); } trace.add("root done"); @@ -395,7 +395,7 @@ public void testExplicitDetachedScopeCancellation() throws Throwable { try { var.get(); trace.add("after get"); - } catch (CancellationException e) { + } catch (CanceledFailure e) { trace.add("scope cancelled"); } trace.add("root done"); @@ -422,7 +422,7 @@ private Promise newTimer(int milliseconds) { try { Workflow.sleep(milliseconds); trace.add("timer fired"); - } catch (CancellationException e) { + } catch (CanceledFailure e) { trace.add("timer cancelled"); throw e; } @@ -523,7 +523,7 @@ public void testExplicitCancellationOnFailure() throws Throwable { "thread1 started", "thread2 started", "thread2 done", - "thread1 exception: CancellationException", + "thread1 exception: CanceledFailure", "thread1 done", "root done" }; @@ -548,7 +548,7 @@ public void testDetachedCancellation() throws Throwable { () -> unblock1 || CancellationScope.current().isCancelRequested()); if (CancellationScope.current().isCancelRequested()) { - done.completeExceptionally(new CancellationException()); + done.completeExceptionally(new CanceledFailure("test")); } else { done.complete(null); } @@ -558,7 +558,7 @@ public void testDetachedCancellation() throws Throwable { .run(); try { done.get(); - } catch (CancellationException e) { + } catch (CanceledFailure e) { trace.add("done cancelled"); } trace.add("root done"); @@ -731,7 +731,7 @@ public void workflowThreadsWillEvictCacheWhenMaxThreadCountIsHit() throws Throwa new DeterministicRunnerImpl( threadPool, new SyncDecisionContext( - decisionContext, GsonJsonDataConverter.getInstance(), null, null), + decisionContext, DataConverter.getDefaultInstance(), null, null), () -> 0L, // clock override () -> { Promise thread = @@ -756,7 +756,7 @@ public void workflowThreadsWillEvictCacheWhenMaxThreadCountIsHit() throws Throwa new DeterministicRunnerImpl( threadPool, new SyncDecisionContext( - decisionContext, GsonJsonDataConverter.getInstance(), null, null), + decisionContext, DataConverter.getDefaultInstance(), null, null), () -> 0L, // clock override () -> { Promise thread = diff --git a/src/test/java/io/temporal/internal/sync/PromiseTest.java b/src/test/java/io/temporal/internal/sync/PromiseTest.java index 9667308f55..e075054ae6 100644 --- a/src/test/java/io/temporal/internal/sync/PromiseTest.java +++ b/src/test/java/io/temporal/internal/sync/PromiseTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.*; +import io.temporal.failure.CanceledFailure; import io.temporal.workflow.CompletablePromise; import io.temporal.workflow.Promise; import io.temporal.workflow.Workflow; @@ -28,7 +29,6 @@ import java.util.Collections; import java.util.IllegalFormatCodePointException; import java.util.List; -import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutorService; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -129,8 +129,8 @@ public void testCancellableGetCancellation() throws Throwable { trace.add("root begin"); try { f.cancellableGet(); - } catch (CancellationException e) { - trace.add("root CancellationException"); + } catch (CanceledFailure e) { + trace.add("root CanceledException"); } trace.add("root done"); }); @@ -139,7 +139,7 @@ public void testCancellableGetCancellation() throws Throwable { r.runUntilAllBlocked(); String[] expected = new String[] { - "root begin", "root CancellationException", "root done", + "root begin", "root CanceledException", "root done", }; trace.setExpected(expected); } @@ -165,7 +165,7 @@ public void testGetTimeout() throws Throwable { assertEquals("bar", f.get(10, TimeUnit.SECONDS)); trace.add("thread1 get success"); fail("failure expected"); - } catch (CancellationException e) { + } catch (CanceledFailure e) { trace.add("thread1 get cancellation"); } catch (TimeoutException e) { trace.add("thread1 get timeout"); diff --git a/src/test/java/io/temporal/internal/sync/SyncDecisionContextTest.java b/src/test/java/io/temporal/internal/sync/SyncDecisionContextTest.java index 6d75a27287..46a06d38b4 100644 --- a/src/test/java/io/temporal/internal/sync/SyncDecisionContextTest.java +++ b/src/test/java/io/temporal/internal/sync/SyncDecisionContextTest.java @@ -21,7 +21,7 @@ import static org.mockito.Mockito.*; -import io.temporal.common.converter.GsonJsonDataConverter; +import io.temporal.common.converter.DataConverter; import io.temporal.internal.common.InternalUtils; import io.temporal.internal.replay.DecisionContext; import io.temporal.proto.common.SearchAttributes; @@ -38,7 +38,7 @@ public class SyncDecisionContextTest { public void setUp() { this.context = new SyncDecisionContext( - mockDecisionContext, GsonJsonDataConverter.getInstance(), null, null); + mockDecisionContext, DataConverter.getDefaultInstance(), null, null); } @Test @@ -46,8 +46,7 @@ public void testUpsertSearchAttributes() throws Throwable { Map attr = new HashMap<>(); attr.put("CustomKeywordField", "keyword"); SearchAttributes serializedAttr = - InternalUtils.convertMapToSearchAttributes( - attr, GsonJsonDataConverter.getInstance().getPayloadConverter()); + InternalUtils.convertMapToSearchAttributes(attr, DataConverter.getDefaultInstance()); context.upsertSearchAttributes(attr); verify(mockDecisionContext, times(1)).upsertSearchAttributes(serializedAttr); diff --git a/src/test/java/io/temporal/internal/sync/WorkflowInternalQueueTest.java b/src/test/java/io/temporal/internal/sync/WorkflowInternalQueueTest.java index b1b7e31e86..3c8590afdd 100644 --- a/src/test/java/io/temporal/internal/sync/WorkflowInternalQueueTest.java +++ b/src/test/java/io/temporal/internal/sync/WorkflowInternalQueueTest.java @@ -22,10 +22,10 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import io.temporal.failure.CanceledFailure; import io.temporal.workflow.QueueConsumer; import io.temporal.workflow.Workflow; import io.temporal.workflow.WorkflowQueue; -import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; import org.junit.Before; import org.junit.Rule; @@ -93,8 +93,8 @@ public void testTakeCancelled() throws Throwable { trace.add("thread1 begin"); try { assertTrue(f.take()); - } catch (CancellationException e) { - trace.add("thread1 CancellationException"); + } catch (CanceledFailure e) { + trace.add("thread1 CanceledException"); } trace.add("thread1 done"); }) @@ -126,8 +126,8 @@ public void testCancellableTakeCancelled() throws Throwable { trace.add("thread1 begin"); try { assertTrue(f.cancellableTake()); - } catch (CancellationException e) { - trace.add("thread1 CancellationException"); + } catch (CanceledFailure e) { + trace.add("thread1 CanceledFailure"); } trace.add("thread1 done"); }) @@ -140,11 +140,7 @@ public void testCancellableTakeCancelled() throws Throwable { String[] expected = new String[] { - "root begin", - "root done", - "thread1 begin", - "thread1 CancellationException", - "thread1 done", + "root begin", "root done", "thread1 begin", "thread1 CanceledFailure", "thread1 done", }; trace.setExpected(expected); } @@ -258,8 +254,8 @@ public void testPutCancelled() throws Throwable { try { f.put(true); f.put(true); - } catch (CancellationException e) { - trace.add("thread1 CancellationException"); + } catch (CanceledFailure e) { + trace.add("thread1 CanceledFailure"); } trace.add("thread1 done"); }) @@ -292,8 +288,8 @@ public void testCancellablePutCancelled() throws Throwable { try { f.put(true); f.cancellablePut(true); - } catch (CancellationException e) { - trace.add("thread1 CancellationException"); + } catch (CanceledFailure e) { + trace.add("thread1 CanceledFailure"); } trace.add("thread1 done"); }) @@ -306,11 +302,7 @@ public void testCancellablePutCancelled() throws Throwable { String[] expected = new String[] { - "root begin", - "root done", - "thread1 begin", - "thread1 CancellationException", - "thread1 done", + "root begin", "root done", "thread1 begin", "thread1 CanceledFailure", "thread1 done", }; trace.setExpected(expected); r.close(); diff --git a/src/test/java/io/temporal/internal/testing/ActivityTestingTest.java b/src/test/java/io/temporal/internal/testing/ActivityTestingTest.java index 6340766dcd..c69bc3f80e 100644 --- a/src/test/java/io/temporal/internal/testing/ActivityTestingTest.java +++ b/src/test/java/io/temporal/internal/testing/ActivityTestingTest.java @@ -25,8 +25,9 @@ import io.temporal.activity.Activity; import io.temporal.activity.ActivityInterface; import io.temporal.client.ActivityCancelledException; +import io.temporal.failure.ActivityFailure; +import io.temporal.failure.ApplicationFailure; import io.temporal.testing.TestActivityEnvironment; -import io.temporal.workflow.ActivityFailureException; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -83,10 +84,15 @@ public void testFailure() { try { activity.activity1("input1"); fail("unreachable"); - } catch (ActivityFailureException e) { + } catch (ActivityFailure e) { assertTrue(e.getMessage().contains("activity1")); - assertTrue(e.getCause() instanceof IOException); - assertEquals("simulated", e.getCause().getMessage()); + assertTrue(e.getCause() instanceof ApplicationFailure); + assertTrue(((ApplicationFailure) e.getCause()).getType().equals(IOException.class.getName())); + + assertEquals( + "message='simulated', type='java.io.IOException', nonRetryable=false", + e.getCause().getMessage()); + e.printStackTrace(); } } diff --git a/src/test/java/io/temporal/internal/testing/WorkflowTestingTest.java b/src/test/java/io/temporal/internal/testing/WorkflowTestingTest.java index e38bb80815..23d6e8e898 100644 --- a/src/test/java/io/temporal/internal/testing/WorkflowTestingTest.java +++ b/src/test/java/io/temporal/internal/testing/WorkflowTestingTest.java @@ -32,10 +32,13 @@ import io.temporal.client.WorkflowException; import io.temporal.client.WorkflowOptions; import io.temporal.client.WorkflowStub; -import io.temporal.client.WorkflowTimedOutException; import io.temporal.common.RetryOptions; import io.temporal.common.context.ContextPropagator; -import io.temporal.common.converter.GsonJsonDataConverter; +import io.temporal.common.converter.DataConverter; +import io.temporal.failure.ActivityFailure; +import io.temporal.failure.CanceledFailure; +import io.temporal.failure.ChildWorkflowFailure; +import io.temporal.failure.TimeoutFailure; import io.temporal.internal.common.WorkflowExecutionUtils; import io.temporal.proto.common.Payload; import io.temporal.proto.common.TimeoutType; @@ -49,15 +52,11 @@ import io.temporal.proto.workflowservice.ListClosedWorkflowExecutionsResponse; import io.temporal.proto.workflowservice.ListOpenWorkflowExecutionsRequest; import io.temporal.proto.workflowservice.ListOpenWorkflowExecutionsResponse; -import io.temporal.testing.SimulatedTimeoutException; import io.temporal.testing.TestEnvironmentOptions; import io.temporal.testing.TestWorkflowEnvironment; import io.temporal.worker.Worker; -import io.temporal.workflow.ActivityFailureException; -import io.temporal.workflow.ActivityTimeoutException; import io.temporal.workflow.Async; import io.temporal.workflow.ChildWorkflowOptions; -import io.temporal.workflow.ChildWorkflowTimedOutException; import io.temporal.workflow.Promise; import io.temporal.workflow.SignalMethod; import io.temporal.workflow.Workflow; @@ -69,12 +68,10 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestWatcher; @@ -169,7 +166,9 @@ public void testFailure() { workflow.workflow1("input1"); fail("unreacheable"); } catch (WorkflowException e) { - assertEquals("TestWorkflow-input1", e.getCause().getMessage()); + assertEquals( + "message='TestWorkflow-input1', type='java.lang.IllegalThreadStateException', nonRetryable=false", + e.getCause().getMessage()); } } @@ -198,7 +197,7 @@ public String workflow1(String input) { Workflow.sleep(Duration.ofHours(1)); // test time skipping try { return activity.activity1(input); - } catch (ActivityFailureException e) { + } catch (ActivityFailure e) { log.info("Failure", e); throw e; } @@ -239,7 +238,8 @@ public void testActivityFailure() { workflow.workflow1("input1"); fail("unreacheable"); } catch (WorkflowException e) { - assertEquals("activity1-input1", e.getCause().getCause().getMessage()); + assertTrue(e.getCause().getCause().getMessage().contains("message='activity1-input1'")); + e.printStackTrace(); } } @@ -247,7 +247,7 @@ private static class SimulatedTimeoutActivityImpl implements TestActivity { @Override public String activity1(String input) { - throw new SimulatedTimeoutException(TimeoutType.Heartbeat, "progress1"); + throw new TimeoutFailure("simulated", "progress1", TimeoutType.ScheduleToClose); } } @@ -264,10 +264,10 @@ public void testActivitySimulatedTimeout() { workflow.workflow1("input1"); fail("unreacheable"); } catch (WorkflowException e) { - assertTrue(e.getCause() instanceof ActivityTimeoutException); - ActivityTimeoutException te = (ActivityTimeoutException) e.getCause(); - assertEquals(TimeoutType.Heartbeat, te.getTimeoutType()); - assertEquals("progress1", te.getDetails(String.class)); + assertTrue(e.getCause() instanceof ActivityFailure); + TimeoutFailure te = (TimeoutFailure) e.getCause().getCause(); + assertEquals(TimeoutType.ScheduleToClose, te.getTimeoutType()); + assertEquals("progress1", te.getLastHeartbeatDetails().get(String.class)); } } @@ -333,9 +333,12 @@ public void testActivityStartToCloseTimeout() { workflow.workflow(10, 10, 1, true); fail("unreacheable"); } catch (WorkflowException e) { - assertTrue(e.getCause() instanceof ActivityTimeoutException); + assertTrue(e.getCause() instanceof ActivityFailure); + assertEquals( + TimeoutType.ScheduleToClose, ((TimeoutFailure) e.getCause().getCause()).getTimeoutType()); assertEquals( - TimeoutType.StartToClose, ((ActivityTimeoutException) e.getCause()).getTimeoutType()); + TimeoutType.StartToClose, + ((TimeoutFailure) e.getCause().getCause().getCause()).getTimeoutType()); } } @@ -352,14 +355,13 @@ public void testActivityScheduleToStartTimeout() { workflow.workflow(10, 1, 10, true); fail("unreacheable"); } catch (WorkflowException e) { - assertTrue(e.getCause() instanceof ActivityTimeoutException); + assertTrue(e.getCause() instanceof ActivityFailure); assertEquals( - TimeoutType.ScheduleToStart, ((ActivityTimeoutException) e.getCause()).getTimeoutType()); + TimeoutType.ScheduleToStart, ((TimeoutFailure) e.getCause().getCause()).getTimeoutType()); } } @Test - @Ignore // ScheduleToClose or StartToClose timeouts should be unified. public void testActivityScheduleToCloseTimeout() { Worker worker = testEnvironment.newWorker(TASK_LIST); worker.registerWorkflowImplementationTypes(TestActivityTimeoutWorkflowImpl.class); @@ -373,9 +375,12 @@ public void testActivityScheduleToCloseTimeout() { workflow.workflow(2, 10, 1, false); fail("unreacheable"); } catch (WorkflowException e) { - assertTrue(e.getCause() instanceof ActivityTimeoutException); + assertTrue(e.getCause() instanceof ActivityFailure); + assertEquals( + TimeoutType.ScheduleToClose, ((TimeoutFailure) e.getCause().getCause()).getTimeoutType()); assertEquals( - TimeoutType.ScheduleToClose, ((ActivityTimeoutException) e.getCause()).getTimeoutType()); + TimeoutType.StartToClose, + ((TimeoutFailure) e.getCause().getCause().getCause()).getTimeoutType()); } } @@ -404,8 +409,8 @@ public void testWorkflowTimeout() { workflow.workflow1("bar"); fail("unreacheable"); } catch (WorkflowException e) { - assertTrue(e instanceof WorkflowTimedOutException); - assertEquals(TimeoutType.StartToClose, ((WorkflowTimedOutException) e).getTimeoutType()); + assertTrue(e instanceof WorkflowException); + assertEquals(TimeoutType.StartToClose, ((TimeoutFailure) e.getCause()).getTimeoutType()); } } @@ -573,7 +578,7 @@ public void testActivityCancellation() { untyped.cancel(); untyped.getResult(String.class); fail("unreacheable"); - } catch (CancellationException e) { + } catch (CanceledFailure e) { } } @@ -708,7 +713,7 @@ public static class SimulatedTimeoutChildWorkflow implements ChildWorkflow { @Override public String workflow(String input, String parentId) { Workflow.sleep(Duration.ofHours(2)); - throw new SimulatedTimeoutException(); + throw new TimeoutFailure("simulated", null, TimeoutType.ScheduleToClose); } } @@ -750,7 +755,8 @@ public void testChildSimulatedTimeout() throws Throwable { } fail("unreacheable"); } catch (WorkflowException e) { - assertTrue(e.getCause() instanceof ChildWorkflowTimedOutException); + assertTrue(e.getCause() instanceof ChildWorkflowFailure); + assertTrue(e.getCause().getCause() instanceof TimeoutFailure); } // List closed workflows and validate their types ListClosedWorkflowExecutionsRequest listRequest = @@ -779,7 +785,8 @@ public void testMockedChildSimulatedTimeout() { ChildWorkflow.class, () -> { ChildWorkflow child = mock(ChildWorkflow.class); - when(child.workflow(anyString(), anyString())).thenThrow(new SimulatedTimeoutException()); + when(child.workflow(anyString(), anyString())) + .thenThrow(new TimeoutFailure("foo", null, TimeoutType.ScheduleToClose)); return child; }); testEnvironment.start(); @@ -791,7 +798,8 @@ public void testMockedChildSimulatedTimeout() { workflow.workflow("input1"); fail("unreacheable"); } catch (WorkflowException e) { - assertTrue(e.getCause() instanceof ChildWorkflowTimedOutException); + assertTrue(e.getCause() instanceof ChildWorkflowFailure); + assertTrue(e.getCause().getCause() instanceof TimeoutFailure); } } @@ -807,8 +815,7 @@ public Map serializeContext(Object context) { String testKey = (String) context; if (testKey != null) { return Collections.singletonMap( - "test", - GsonJsonDataConverter.getInstance().getPayloadConverter().toData(testKey).get()); + "test", DataConverter.getDefaultInstance().toPayload(testKey).get()); } else { return Collections.emptyMap(); } @@ -817,9 +824,8 @@ public Map serializeContext(Object context) { @Override public Object deserializeContext(Map context) { if (context.containsKey("test")) { - return GsonJsonDataConverter.getInstance() - .getPayloadConverter() - .fromData(context.get("test"), String.class, String.class); + return DataConverter.getDefaultInstance() + .fromPayload(context.get("test"), String.class, String.class); } else { return null; diff --git a/src/test/java/io/temporal/worker/CleanWorkerShutdownTest.java b/src/test/java/io/temporal/worker/CleanWorkerShutdownTest.java index 65b5b10a7a..da5e80dee8 100644 --- a/src/test/java/io/temporal/worker/CleanWorkerShutdownTest.java +++ b/src/test/java/io/temporal/worker/CleanWorkerShutdownTest.java @@ -30,7 +30,7 @@ import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientOptions; import io.temporal.client.WorkflowOptions; -import io.temporal.common.converter.GsonJsonDataConverter; +import io.temporal.common.converter.DataConverter; import io.temporal.proto.common.Payloads; import io.temporal.proto.common.WorkflowExecution; import io.temporal.proto.event.EventType; @@ -201,8 +201,8 @@ public void testShutdown() throws ExecutionException, InterruptedException { found = true; Payloads ar = e.getActivityTaskCompletedEventAttributes().getResult(); String r = - GsonJsonDataConverter.getInstance() - .fromData(Optional.of(ar), String.class, String.class); + DataConverter.getDefaultInstance() + .fromPayloads(Optional.of(ar), String.class, String.class); assertEquals("completed", r); } } @@ -262,8 +262,8 @@ public void testShutdownNow() throws ExecutionException, InterruptedException { found = true; Payloads ar = e.getActivityTaskCompletedEventAttributes().getResult(); String r = - GsonJsonDataConverter.getInstance() - .fromData(Optional.of(ar), String.class, String.class); + DataConverter.getDefaultInstance() + .fromPayloads(Optional.of(ar), String.class, String.class); assertEquals(r, "interrupted", r); } } @@ -353,8 +353,8 @@ public void testShutdownHeartbeatingActivity() throws ExecutionException, Interr found = true; Payloads ar = e.getActivityTaskCompletedEventAttributes().getResult(); String r = - GsonJsonDataConverter.getInstance() - .fromData(Optional.of(ar), String.class, String.class); + DataConverter.getDefaultInstance() + .fromPayloads(Optional.of(ar), String.class, String.class); assertEquals("workershutdown", r); } } diff --git a/src/test/java/io/temporal/worker/StickyWorkerTest.java b/src/test/java/io/temporal/worker/StickyWorkerTest.java index 5c36b88fa2..570a0b2601 100644 --- a/src/test/java/io/temporal/worker/StickyWorkerTest.java +++ b/src/test/java/io/temporal/worker/StickyWorkerTest.java @@ -226,7 +226,7 @@ public void workflowCacheEvictionDueToThreads() { int count = 100; ActivitiesWorkflow[] workflows = new ActivitiesWorkflow[count]; WorkflowParams w = new WorkflowParams(); - w.TemporalSleep = Duration.ofSeconds(1); + w.TemporalSleepMillis = 1000; w.ChainSequence = 2; w.ConcurrentCount = 1; w.PayloadSizeBytes = 10; @@ -276,7 +276,7 @@ public void whenStickyIsEnabledThenTheWorkflowIsCachedActivities() throws Except // Act WorkflowParams w = new WorkflowParams(); - w.TemporalSleep = Duration.ofSeconds(1); + w.TemporalSleepMillis = 1000; w.ChainSequence = 2; w.ConcurrentCount = 1; w.PayloadSizeBytes = 10; @@ -468,10 +468,13 @@ public void workflowsCanBeQueried() throws Exception { // Act WorkflowClient.start(workflow::getGreeting); - Thread.sleep(200); // Wait for workflow to start - + // Wait for the first decision to go through DeciderCache cache = factory.getCache(); assertNotNull(cache); + long start = System.currentTimeMillis(); + while (cache.size() == 0 && System.currentTimeMillis() - start < 5000) { + Thread.sleep(200); + } assertEquals(1, cache.size()); // Assert @@ -585,7 +588,7 @@ public static class WorkflowParams { public int ConcurrentCount; public String TaskListName; public int PayloadSizeBytes; - public Duration TemporalSleep; // nano + public long TemporalSleepMillis; } @WorkflowInterface @@ -699,7 +702,7 @@ public void execute(WorkflowParams params) { promise.get(); } - Workflow.sleep(params.TemporalSleep); + Workflow.sleep(params.TemporalSleepMillis); } } } diff --git a/src/test/java/io/temporal/worker/WorkerStressTests.java b/src/test/java/io/temporal/worker/WorkerStressTests.java index 00b1bac862..56ea84fbdb 100644 --- a/src/test/java/io/temporal/worker/WorkerStressTests.java +++ b/src/test/java/io/temporal/worker/WorkerStressTests.java @@ -101,7 +101,7 @@ public void longHistoryWorkflowsCompleteSuccessfully() throws InterruptedExcepti // Act // This will yeild around 10000 events which is above the page limit returned by the server. WorkflowParams w = new WorkflowParams(); - w.TemporalSleep = Duration.ofSeconds(0); + w.TemporalSleepSeconds = 0; w.ChainSequence = 50; w.ConcurrentCount = 50; w.PayloadSizeBytes = 10000; @@ -139,7 +139,7 @@ public void selfEvictionDoesNotCauseDeadlock() throws InterruptedException { // Act WorkflowParams w = new WorkflowParams(); - w.TemporalSleep = Duration.ofSeconds(0); + w.TemporalSleepSeconds = 0; w.ChainSequence = 1; w.ConcurrentCount = 15; w.PayloadSizeBytes = 100; @@ -213,7 +213,7 @@ public static class WorkflowParams { public int ConcurrentCount; public String TaskListName; public int PayloadSizeBytes; - public Duration TemporalSleep; + public int TemporalSleepSeconds; } @WorkflowInterface @@ -250,7 +250,7 @@ public String execute(WorkflowParams params) { promise.get(); } - Workflow.sleep(params.TemporalSleep); + Workflow.sleep(Duration.ofSeconds(params.TemporalSleepSeconds)); } return "I'm done"; } diff --git a/src/test/java/io/temporal/workflow/MetricsTest.java b/src/test/java/io/temporal/workflow/MetricsTest.java index bcad3aca00..dc9a12d09b 100644 --- a/src/test/java/io/temporal/workflow/MetricsTest.java +++ b/src/test/java/io/temporal/workflow/MetricsTest.java @@ -106,7 +106,7 @@ public void execute() { .setMaximumInterval(Duration.ofSeconds(1)) .setInitialInterval(Duration.ofSeconds(1)) .setMaximumAttempts(3) - .setDoNotRetry(AssertionError.class) + .setDoNotRetry(AssertionError.class.getName()) .build()) .build(); TestActivity activity = Workflow.newActivityStub(TestActivity.class, activityOptions); diff --git a/src/test/java/io/temporal/workflow/WorkflowTest.java b/src/test/java/io/temporal/workflow/WorkflowTest.java index 8cfeeef74e..dac966442e 100644 --- a/src/test/java/io/temporal/workflow/WorkflowTest.java +++ b/src/test/java/io/temporal/workflow/WorkflowTest.java @@ -40,26 +40,32 @@ import io.temporal.client.ActivityCompletionClient; import io.temporal.client.ActivityNotExistsException; import io.temporal.client.BatchRequest; -import io.temporal.client.DuplicateWorkflowException; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowClientInterceptorBase; import io.temporal.client.WorkflowClientOptions; import io.temporal.client.WorkflowException; -import io.temporal.client.WorkflowFailureException; +import io.temporal.client.WorkflowExecutionAlreadyStarted; +import io.temporal.client.WorkflowFailedException; import io.temporal.client.WorkflowOptions; import io.temporal.client.WorkflowQueryException; import io.temporal.client.WorkflowQueryRejectedException; import io.temporal.client.WorkflowStub; -import io.temporal.client.WorkflowTimedOutException; import io.temporal.common.CronSchedule; import io.temporal.common.MethodRetry; import io.temporal.common.RetryOptions; +import io.temporal.common.converter.DataConverter; import io.temporal.common.converter.GsonJsonPayloadConverter; import io.temporal.common.interceptors.BaseWorkflowInvoker; import io.temporal.common.interceptors.WorkflowCallsInterceptor; import io.temporal.common.interceptors.WorkflowInterceptor; import io.temporal.common.interceptors.WorkflowInvocationInterceptor; import io.temporal.common.interceptors.WorkflowInvoker; +import io.temporal.failure.ActivityFailure; +import io.temporal.failure.ApplicationFailure; +import io.temporal.failure.CanceledFailure; +import io.temporal.failure.ChildWorkflowFailure; +import io.temporal.failure.TimeoutFailure; +import io.temporal.internal.common.SearchAttributesUtil; import io.temporal.internal.common.WorkflowExecutionHistory; import io.temporal.internal.common.WorkflowExecutionUtils; import io.temporal.internal.sync.DeterministicRunnerTest; @@ -71,7 +77,6 @@ import io.temporal.proto.common.WorkflowIdReusePolicy; import io.temporal.proto.event.EventType; import io.temporal.proto.event.HistoryEvent; -import io.temporal.proto.event.WorkflowExecutionFailedCause; import io.temporal.proto.execution.WorkflowExecutionStatus; import io.temporal.proto.query.QueryRejectCondition; import io.temporal.proto.workflowservice.GetWorkflowExecutionHistoryRequest; @@ -115,7 +120,6 @@ import java.util.Random; import java.util.Set; import java.util.UUID; -import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -504,7 +508,7 @@ public String execute(String taskList) { .setMaximumInterval(Duration.ofSeconds(1)) .setInitialInterval(Duration.ofSeconds(1)) .setMaximumAttempts(3) - .setDoNotRetry(AssertionError.class) + .setDoNotRetry(AssertionError.class.getName()) .build()) .build(); TestActivities activities = Workflow.newActivityStub(TestActivities.class, options); @@ -530,7 +534,10 @@ public void testActivityRetryWithMaxAttempts() { workflowStub.execute(taskList); fail("unreachable"); } catch (WorkflowException e) { - assertTrue(e.getCause().getCause() instanceof IOException); + assertTrue(e.getCause() instanceof ActivityFailure); + assertTrue(e.getCause().getCause() instanceof ApplicationFailure); + assertEquals( + IOException.class.getName(), ((ApplicationFailure) e.getCause().getCause()).getType()); } assertEquals(activitiesImpl.toString(), 3, activitiesImpl.invocations.size()); } @@ -549,7 +556,7 @@ public String execute(String taskList) { RetryOptions.newBuilder() .setMaximumInterval(Duration.ofSeconds(1)) .setInitialInterval(Duration.ofSeconds(1)) - .setDoNotRetry(AssertionError.class) + .setDoNotRetry(AssertionError.class.getName()) .build()) .build(); TestActivities activities = Workflow.newActivityStub(TestActivities.class, options); @@ -575,7 +582,10 @@ public void testActivityRetryWithExiration() { workflowStub.execute(taskList); fail("unreachable"); } catch (WorkflowException e) { - assertTrue(e.getCause().getCause() instanceof IOException); + assertTrue(e.getCause() instanceof ActivityFailure); + assertTrue(e.getCause().getCause() instanceof ApplicationFailure); + assertEquals( + IOException.class.getName(), ((ApplicationFailure) e.getCause().getCause()).getType()); } assertEquals(activitiesImpl.toString(), 3, activitiesImpl.invocations.size()); } @@ -594,7 +604,7 @@ public String execute(String taskList) { .setMaximumInterval(Duration.ofSeconds(1)) .setInitialInterval(Duration.ofSeconds(1)) .setMaximumAttempts(5) - .setDoNotRetry(AssertionError.class) + .setDoNotRetry(AssertionError.class.getName()) .build()) .build(); TestActivities activities = Workflow.newLocalActivityStub(TestActivities.class, options); @@ -617,8 +627,10 @@ public void testLocalActivityRetry() { workflowStub.execute(taskList); fail("unreachable"); } catch (WorkflowException e) { - assertNotNull(e.toString(), e.getCause()); - assertTrue(e.getCause().getCause() instanceof IOException); + assertTrue(e.getCause() instanceof ActivityFailure); + assertTrue(e.getCause().getCause() instanceof ApplicationFailure); + assertEquals( + IOException.class.getName(), ((ApplicationFailure) e.getCause().getCause()).getType()); } assertEquals(activitiesImpl.toString(), 5, activitiesImpl.invocations.size()); assertEquals("last attempt", 5, activitiesImpl.getLastAttempt()); @@ -639,7 +651,7 @@ public String execute(String taskList) { .setMaximumInterval(Duration.ofSeconds(1)) .setInitialInterval(Duration.ofSeconds(1)) .setMaximumAttempts(3) - .setDoNotRetry(AssertionError.class) + .setDoNotRetry(AssertionError.class.getName()) .build()) .build(); TestActivities activities = Workflow.newActivityStub(TestActivities.class, options); @@ -647,7 +659,7 @@ public String execute(String taskList) { try { activities.neverComplete(); // should timeout as scheduleToClose is 1 second throw new IllegalStateException("unreachable"); - } catch (ActivityTimeoutException e) { + } catch (ActivityFailure e) { long elapsed = Workflow.currentTimeMillis() - start; if (elapsed < 5000) { throw new RuntimeException("Activity retried without delay: " + elapsed); @@ -669,7 +681,8 @@ public void testActivityRetryOnTimeout() { workflowStub.execute(taskList); fail("unreachable"); } catch (WorkflowException e) { - assertTrue(String.valueOf(e.getCause()), e.getCause() instanceof ActivityTimeoutException); + assertTrue(String.valueOf(e.getCause()), e.getCause() instanceof ActivityFailure); + assertTrue(String.valueOf(e.getCause()), e.getCause().getCause() instanceof TimeoutFailure); } assertEquals(activitiesImpl.toString(), 3, activitiesImpl.invocations.size()); long elapsed = System.currentTimeMillis() - start; @@ -722,9 +735,9 @@ public void testActivityRetryOptionsChange() { workflowStub.execute(taskList); fail("unreachable"); } catch (WorkflowException e) { - assertTrue( - String.valueOf(Throwables.getStackTraceAsString(e)), - e.getCause().getCause() instanceof IOException); + assertTrue(e.getCause().getCause() instanceof ApplicationFailure); + assertEquals( + IOException.class.getName(), ((ApplicationFailure) e.getCause().getCause()).getType()); } assertEquals(activitiesImpl.toString(), 2, activitiesImpl.invocations.size()); } @@ -763,7 +776,10 @@ public void testUntypedActivityRetry() { workflowStub.execute(taskList); fail("unreachable"); } catch (WorkflowException e) { - assertTrue(e.getCause().getCause() instanceof IOException); + assertTrue(e.getCause() instanceof ActivityFailure); + assertTrue(e.getCause().getCause() instanceof ApplicationFailure); + assertEquals( + IOException.class.getName(), ((ApplicationFailure) e.getCause().getCause()).getType()); } assertEquals(activitiesImpl.toString(), 3, activitiesImpl.invocations.size()); } @@ -801,11 +817,54 @@ public void testActivityRetryAnnotated() { workflowStub.execute(taskList); fail("unreachable"); } catch (WorkflowException e) { - assertTrue(e.getCause().getCause() instanceof IOException); + assertTrue(e.getCause() instanceof ActivityFailure); + assertTrue(e.getCause().getCause() instanceof ApplicationFailure); + assertEquals( + IOException.class.getName(), ((ApplicationFailure) e.getCause().getCause()).getType()); } assertEquals(activitiesImpl.toString(), 3, activitiesImpl.invocations.size()); } + public static class TestActivityApplicationFailureRetry implements TestWorkflow1 { + + private TestActivities activities; + + @Override + public String execute(String taskList) { + ActivityOptions options = + ActivityOptions.newBuilder() + .setTaskList(taskList) + .setScheduleToCloseTimeout(Duration.ofSeconds(200)) + .setStartToCloseTimeout(Duration.ofSeconds(1)) + .setRetryOptions( + RetryOptions.newBuilder().setMaximumInterval(Duration.ofSeconds(1)).build()) + .build(); + activities = Workflow.newActivityStub(TestActivities.class, options); + activities.throwApplicationFailureThreeTimes(); + return "ignored"; + } + } + + @Test + public void testActivityApplicationFailureRetry() { + startWorkerFor(TestActivityApplicationFailureRetry.class); + TestWorkflow1 workflowStub = + workflowClient.newWorkflowStub( + TestWorkflow1.class, newWorkflowOptionsBuilder(taskList).build()); + try { + workflowStub.execute(taskList); + fail("unreachable"); + } catch (WorkflowException e) { + assertTrue(e.getCause() instanceof ActivityFailure); + assertTrue(e.getCause().getCause() instanceof ApplicationFailure); + assertEquals("simulatedType", ((ApplicationFailure) e.getCause().getCause()).getType()); + assertEquals( + "io.temporal.failure.ApplicationFailure: message='simulated', type='simulatedType', nonRetryable=true", + e.getCause().getCause().toString()); + } + assertEquals(3, activitiesImpl.applicationFailureCounter.get()); + } + public static class TestAsyncActivityRetry implements TestWorkflow1 { private TestActivities activities; @@ -842,7 +901,9 @@ public void testAsyncActivityRetry() throws IOException { workflowStub.execute(taskList); fail("unreachable"); } catch (WorkflowException e) { - assertTrue(e.getCause().getCause() instanceof IOException); + assertTrue(e.getCause().getCause() instanceof ApplicationFailure); + assertEquals( + IOException.class.getName(), ((ApplicationFailure) e.getCause().getCause()).getType()); } assertEquals(activitiesImpl.toString(), 3, activitiesImpl.invocations.size()); WorkflowExecution execution = WorkflowStub.fromTyped(workflowStub).getExecution(); @@ -899,7 +960,7 @@ public String execute(String taskList) { RetryOptions.newBuilder() .setMaximumInterval(Duration.ofSeconds(1)) .setInitialInterval(Duration.ofSeconds(1)) - .setDoNotRetry(NullPointerException.class) + .setDoNotRetry(NullPointerException.class.getName()) .setMaximumAttempts(3) .build()); } else { @@ -908,7 +969,7 @@ public String execute(String taskList) { .setMaximumInterval(Duration.ofSeconds(1)) .setInitialInterval(Duration.ofSeconds(1)) .setMaximumAttempts(2) - .setDoNotRetry(NullPointerException.class) + .setDoNotRetry(NullPointerException.class.getName()) .build()); } this.activities = Workflow.newActivityStub(TestActivities.class, options.build()); @@ -927,7 +988,10 @@ public void testAsyncActivityRetryOptionsChange() { workflowStub.execute(taskList); fail("unreachable"); } catch (WorkflowException e) { - assertTrue(e.getCause().getCause() instanceof IOException); + assertTrue(e.getCause() instanceof ActivityFailure); + assertTrue(e.getCause().getCause() instanceof ApplicationFailure); + assertEquals( + IOException.class.getName(), ((ApplicationFailure) e.getCause().getCause()).getType()); } assertEquals(activitiesImpl.toString(), 2, activitiesImpl.invocations.size()); } @@ -947,10 +1011,13 @@ public String execute(String taskList) { try { // false for second argument means to heartbeat once to set details and then stop. activities.activityWithDelay(5000, false); - } catch (ActivityTimeoutException e) { + } catch (ActivityFailure e) { + TimeoutFailure te = (TimeoutFailure) e.getCause(); log.info("TestHeartbeatTimeoutDetails expected timeout", e); - assertEquals(TimeoutType.Heartbeat, e.getTimeoutType()); - return e.getDetails(String.class); + assertEquals(TimeoutType.ScheduleToClose, te.getTimeoutType()); + assertTrue(te.getCause() instanceof TimeoutFailure); + assertEquals(TimeoutType.Heartbeat, ((TimeoutFailure) te.getCause()).getTimeoutType()); + return (te.getLastHeartbeatDetails().get(String.class)); } throw new RuntimeException("unreachable"); } @@ -1016,7 +1083,7 @@ public void workflowsWithFailedPromisesCanBeCancelled() { try { client.getResult(String.class); fail("unreachable"); - } catch (CancellationException ignored) { + } catch (CanceledFailure ignored) { } } @@ -1031,7 +1098,7 @@ public void testWorkflowCancellation() { try { client.getResult(String.class); fail("unreachable"); - } catch (CancellationException ignored) { + } catch (CanceledFailure ignored) { } } @@ -1056,7 +1123,7 @@ public void testWorkflowCancellationScopePromise() { try { client.getResult(String.class); fail("unreachable"); - } catch (CancellationException ignored) { + } catch (CanceledFailure ignored) { } } @@ -1069,14 +1136,14 @@ public String execute(String taskList) { try { testActivities.activityWithDelay(100000, true); fail("unreachable"); - } catch (CancellationException e) { + } catch (CanceledFailure e) { Workflow.newDetachedCancellationScope(() -> assertEquals(1, testActivities.activity1(1))) .run(); } try { Workflow.sleep(Duration.ofHours(1)); fail("unreachable"); - } catch (CancellationException e) { + } catch (CanceledFailure e) { Workflow.newDetachedCancellationScope( () -> assertEquals("a12", testActivities.activity2("a1", 2))) .run(); @@ -1085,7 +1152,7 @@ public String execute(String taskList) { try { Workflow.newTimer(Duration.ofHours(1)).get(); fail("unreachable"); - } catch (CancellationException e) { + } catch (CanceledFailure e) { Workflow.newDetachedCancellationScope( () -> assertEquals("a123", testActivities.activity3("a1", 2, 3))) .run(); @@ -1107,7 +1174,7 @@ public void testDetachedScope() { try { client.getResult(String.class); fail("unreachable"); - } catch (CancellationException ignored) { + } catch (CanceledFailure ignored) { } activitiesImpl.assertInvocations("activityWithDelay", "activity1", "activity2", "activity3"); } @@ -1173,7 +1240,7 @@ public static class TestChildWorkflowImpl implements TestChildWorkflow { public void execute() { try { Workflow.sleep(Duration.ofHours(1)); - } catch (CancellationException e) { + } catch (CanceledFailure e) { Workflow.newDetachedCancellationScope( () -> { Workflow.sleep(Duration.ofSeconds(1)); @@ -1198,7 +1265,7 @@ public void testTryCancelActivity() { try { stub.getResult(String.class); fail("unreachable"); - } catch (CancellationException ignored) { + } catch (CanceledFailure ignored) { } long elapsed = currentTimeMillis() - start; assertTrue(elapsed < 500); @@ -1220,7 +1287,7 @@ public void testAbandonOnCancelActivity() { try { stub.getResult(String.class); fail("unreachable"); - } catch (CancellationException ignored) { + } catch (CanceledFailure ignored) { } long elapsed = currentTimeMillis() - start; assertTrue(elapsed < 500); @@ -1264,7 +1331,7 @@ public void testChildWorkflowWaitCancellationRequested() { try { client.getResult(String.class); fail("unreachable"); - } catch (CancellationException ignored) { + } catch (CanceledFailure ignored) { } GetWorkflowExecutionHistoryRequest request = GetWorkflowExecutionHistoryRequest.newBuilder() @@ -1301,7 +1368,7 @@ public void testChildWorkflowWaitCancellationCompleted() { try { client.getResult(String.class); fail("unreachable"); - } catch (CancellationException ignored) { + } catch (CanceledFailure ignored) { } GetWorkflowExecutionHistoryRequest request = GetWorkflowExecutionHistoryRequest.newBuilder() @@ -1332,7 +1399,7 @@ public void testChildWorkflowCancellationAbandon() { try { client.getResult(String.class); fail("unreachable"); - } catch (CancellationException ignored) { + } catch (CanceledFailure ignored) { } GetWorkflowExecutionHistoryRequest request = GetWorkflowExecutionHistoryRequest.newBuilder() @@ -1363,7 +1430,7 @@ public void testChildWorkflowCancellationTryCancel() { try { client.getResult(String.class); fail("unreachable"); - } catch (CancellationException ignored) { + } catch (CanceledFailure ignored) { } GetWorkflowExecutionHistoryRequest request = GetWorkflowExecutionHistoryRequest.newBuilder() @@ -1642,7 +1709,7 @@ public void testStart() { try { stubF2.func2("1", 2); fail("unreachable"); - } catch (DuplicateWorkflowException e) { + } catch (IllegalStateException e) { // expected } TestMultiargsWorkflowsFunc3 stubF3 = @@ -1753,29 +1820,25 @@ public void testSearchAttributes() { Map fieldsMap = searchAttrFromEvent.getIndexedFieldsMap(); Payload searchAttrStringBytes = fieldsMap.get(testKeyString); + DataConverter converter = DataConverter.getDefaultInstance(); String retrievedString = - GsonJsonPayloadConverter.getInstance() - .fromData(searchAttrStringBytes, String.class, String.class); + converter.fromPayload(searchAttrStringBytes, String.class, String.class); assertEquals(testValueString, retrievedString); Payload searchAttrIntegerBytes = fieldsMap.get(testKeyInteger); Integer retrievedInteger = - GsonJsonPayloadConverter.getInstance() - .fromData(searchAttrIntegerBytes, Integer.class, Integer.class); + converter.fromPayload(searchAttrIntegerBytes, Integer.class, Integer.class); assertEquals(testValueInteger, retrievedInteger); Payload searchAttrDateTimeBytes = fieldsMap.get(testKeyDateTime); LocalDateTime retrievedDateTime = - GsonJsonPayloadConverter.getInstance() - .fromData(searchAttrDateTimeBytes, LocalDateTime.class, LocalDateTime.class); + converter.fromPayload(searchAttrDateTimeBytes, LocalDateTime.class, LocalDateTime.class); assertEquals(testValueDateTime, retrievedDateTime); Payload searchAttrBoolBytes = fieldsMap.get(testKeyBool); Boolean retrievedBool = - GsonJsonPayloadConverter.getInstance() - .fromData(searchAttrBoolBytes, Boolean.class, Boolean.class); + converter.fromPayload(searchAttrBoolBytes, Boolean.class, Boolean.class); assertEquals(testValueBool, retrievedBool); Payload searchAttrDoubleBytes = fieldsMap.get(testKeyDouble); Double retrievedDouble = - GsonJsonPayloadConverter.getInstance() - .fromData(searchAttrDoubleBytes, Double.class, Double.class); + converter.fromPayload(searchAttrDoubleBytes, Double.class, Double.class); assertEquals(testValueDouble, retrievedDouble); } } @@ -2317,8 +2380,13 @@ public void testAsyncRetry() { result = client.execute(useExternalService); fail("unreachable"); } catch (WorkflowException e) { - assertTrue(e.getCause() instanceof IllegalThreadStateException); - assertEquals("simulated", e.getCause().getMessage()); + assertTrue(e.getCause() instanceof ApplicationFailure); + assertEquals( + IllegalThreadStateException.class.getName(), + ((ApplicationFailure) e.getCause()).getType()); + assertEquals( + "message='simulated', type='java.lang.IllegalThreadStateException', nonRetryable=false", + e.getCause().getMessage()); } assertNull(result); List trace = client.getTrace(); @@ -2385,8 +2453,13 @@ public void testAsyncRetryOptionsChange() { result = client.execute(useExternalService); fail("unreachable"); } catch (WorkflowException e) { - assertTrue(e.getCause() instanceof IllegalThreadStateException); - assertEquals("simulated", e.getCause().getMessage()); + assertTrue(e.getCause() instanceof ApplicationFailure); + assertEquals( + IllegalThreadStateException.class.getName(), + ((ApplicationFailure) e.getCause()).getType()); + assertEquals( + "message='simulated', type='java.lang.IllegalThreadStateException', nonRetryable=false", + e.getCause().getMessage()); } assertNull(result); List trace = client.getTrace(); @@ -2413,11 +2486,14 @@ public String execute(String taskList) { testActivities.throwIO(); fail("unreachable"); return "ignored"; - } catch (ActivityFailureException e) { + } catch (ActivityFailure e) { try { assertTrue(e.getMessage().contains("throwIO")); - assertTrue(e.getCause() instanceof IOException); - assertEquals("simulated IO problem", e.getCause().getMessage()); + assertTrue(e.getCause() instanceof ApplicationFailure); + assertEquals(IOException.class.getName(), ((ApplicationFailure) e.getCause()).getType()); + assertEquals( + "message='simulated IO problem', type='java.io.IOException', nonRetryable=false", + e.getCause().getMessage()); } catch (AssertionError ae) { // Errors cause decision to fail. But we want workflow to fail in this case. throw new RuntimeException(ae); @@ -2444,11 +2520,18 @@ public void execute(String taskList) { try { assertNoEmptyStacks(e); assertTrue(e.getMessage().contains("TestWorkflow1")); - assertTrue(e instanceof ChildWorkflowException); - assertTrue(e.getCause() instanceof NumberFormatException); - assertTrue(e.getCause().getCause() instanceof ActivityFailureException); - assertTrue(e.getCause().getCause().getCause() instanceof IOException); - assertEquals("simulated IO problem", e.getCause().getCause().getCause().getMessage()); + assertTrue(e instanceof ChildWorkflowFailure); + assertTrue(e.getCause() instanceof ApplicationFailure); + assertEquals( + NumberFormatException.class.getName(), ((ApplicationFailure) e.getCause()).getType()); + assertTrue(e.getCause().getCause() instanceof ActivityFailure); + assertTrue(e.getCause().getCause().getCause() instanceof ApplicationFailure); + assertEquals( + IOException.class.getName(), + ((ApplicationFailure) e.getCause().getCause().getCause()).getType()); + assertEquals( + "message='simulated IO problem', type='java.io.IOException', nonRetryable=false", + e.getCause().getCause().getCause().getMessage()); } catch (AssertionError ae) { // Errors cause decision to fail. But we want workflow to fail in this case. throw new RuntimeException(ae); @@ -2475,9 +2558,9 @@ private static void assertNoEmptyStacks(RuntimeException e) { * through a WorkflowClient: * *

    -   * {@link WorkflowFailureException}
    -   *     ->{@link ChildWorkflowFailureException}
    -   *         ->{@link ActivityFailureException}
    +   * {@link WorkflowFailedException}
    +   *     ->{@link ChildWorkflowFailure}
    +   *         ->{@link ActivityFailure}
        *             ->OriginalActivityException
        * 
    * @@ -2493,7 +2576,7 @@ public void testExceptionPropagation() { try { client.execute(taskList); fail("Unreachable"); - } catch (WorkflowFailureException e) { + } catch (WorkflowFailedException e) { // Rethrow the assertion failure if (e.getCause().getCause() instanceof AssertionError) { throw (AssertionError) e.getCause().getCause(); @@ -2503,13 +2586,23 @@ public void testExceptionPropagation() { // e.printStackTrace(); assertTrue(e.getMessage(), e.getMessage().contains("TestExceptionPropagation")); assertTrue(e.getStackTrace().length > 0); - assertTrue(e.getCause() instanceof FileNotFoundException); - assertTrue(e.getCause().getCause() instanceof ChildWorkflowException); - assertTrue(e.getCause().getCause().getCause() instanceof NumberFormatException); - assertTrue(e.getCause().getCause().getCause().getCause() instanceof ActivityFailureException); - assertTrue(e.getCause().getCause().getCause().getCause().getCause() instanceof IOException); + assertTrue(e.getCause() instanceof ApplicationFailure); + assertEquals( + FileNotFoundException.class.getName(), ((ApplicationFailure) e.getCause()).getType()); + assertTrue(e.getCause().getCause() instanceof ChildWorkflowFailure); + assertTrue(e.getCause().getCause().getCause() instanceof ApplicationFailure); + assertEquals( + NumberFormatException.class.getName(), + ((ApplicationFailure) e.getCause().getCause().getCause()).getType()); + assertTrue(e.getCause().getCause().getCause().getCause() instanceof ActivityFailure); + assertTrue( + e.getCause().getCause().getCause().getCause().getCause() instanceof ApplicationFailure); assertEquals( - "simulated IO problem", + IOException.class.getName(), + ((ApplicationFailure) e.getCause().getCause().getCause().getCause().getCause()) + .getType()); + assertEquals( + "message='simulated IO problem', type='java.io.IOException', nonRetryable=false", e.getCause().getCause().getCause().getCause().getCause().getMessage()); } } @@ -2693,7 +2786,7 @@ public void testSignalWithStart() throws Exception { try { workflowClient.signalWithStart(batch4); fail("DuplicateWorkflowException expected"); - } catch (DuplicateWorkflowException e) { + } catch (WorkflowExecutionAlreadyStarted e) { assertEquals(execution3.getRunId(), e.getExecution().getRunId()); } } @@ -2934,7 +3027,7 @@ public String execute(String taskList) { try { child.execute("Hello ", (int) Duration.ofDays(1).toMillis()); } catch (Exception e) { - return e.getClass().getSimpleName(); + return Throwables.getStackTraceAsString(e); } throw new RuntimeException("not reachable"); } @@ -2984,7 +3077,9 @@ public void testChildWorkflowTimeout() { .setTaskList(taskList) .build(); TestWorkflow1 client = workflowClient.newWorkflowStub(TestWorkflow1.class, options); - assertEquals("ChildWorkflowTimedOutException", client.execute(taskList)); + String result = client.execute(taskList); + assertTrue(result, result.contains("ChildWorkflowFailure")); + assertTrue(result, result.contains("TimeoutFailure")); } public static class TestParentWorkflowContinueAsNew implements TestWorkflow1 { @@ -3078,8 +3173,8 @@ public void testChildAlreadyRunning() { try { client.execute(false, WorkflowIdReusePolicy.RejectDuplicate); fail("unreachable"); - } catch (WorkflowFailureException e) { - assertTrue(e.getCause() instanceof StartChildWorkflowFailedException); + } catch (WorkflowFailedException e) { + assertTrue(e.getCause() instanceof ChildWorkflowFailure); } } @@ -3098,8 +3193,8 @@ public void testChildStartTwice() { try { client.execute(true, WorkflowIdReusePolicy.RejectDuplicate); fail("unreachable"); - } catch (WorkflowFailureException e) { - assertTrue(e.getCause() instanceof StartChildWorkflowFailedException); + } catch (WorkflowFailedException e) { + assertTrue(e.getCause() instanceof ChildWorkflowFailure); } } @@ -3197,10 +3292,16 @@ public void testChildWorkflowRetry() { try { client.execute(taskList); fail("unreachable"); - } catch (WorkflowException e) { - assertTrue(e.toString(), e.getCause() instanceof ChildWorkflowFailureException); - assertTrue(e.toString(), e.getCause().getCause() instanceof UnsupportedOperationException); - assertEquals("simulated failure", e.getCause().getCause().getMessage()); + } catch (WorkflowFailedException e) { + e.printStackTrace(); + assertTrue(e.toString(), e.getCause() instanceof ChildWorkflowFailure); + assertTrue(e.toString(), e.getCause().getCause() instanceof ApplicationFailure); + assertEquals( + UnsupportedOperationException.class.getName(), + ((ApplicationFailure) e.getCause().getCause()).getType()); + assertEquals( + "message='simulated failure', type='java.lang.UnsupportedOperationException', nonRetryable=false", + e.getCause().getCause().getMessage()); } assertEquals("TestWorkflow1", lastStartedWorkflowType.get()); assertEquals(3, angryChildActivity.getInvocationCount()); @@ -3396,14 +3497,12 @@ public void testSignalExternalWorkflowFailure() { try { client.execute(taskList); fail("unreachable"); - } catch (WorkflowFailureException e) { - assertTrue(e.getCause() instanceof SignalExternalWorkflowException); - assertEquals( - "invalid id", - ((SignalExternalWorkflowException) e.getCause()).getSignaledExecution().getWorkflowId()); + } catch (WorkflowFailedException e) { + assertTrue(e.getCause() instanceof ApplicationFailure); assertEquals( - WorkflowExecutionFailedCause.UnknownExternalWorkflowExecution, - ((SignalExternalWorkflowException) e.getCause()).getFailureCause()); + SignalExternalWorkflowException.class.getName(), + ((ApplicationFailure) e.getCause()).getType()); + assertTrue(e.getCause().getMessage().contains("invalid id")); } } @@ -3443,8 +3542,8 @@ public void testSignalExternalWorkflowImmediateCancellation() { try { client.execute(taskList); fail("unreachable"); - } catch (WorkflowFailureException e) { - assertTrue(e.getCause() instanceof CancellationException); + } catch (WorkflowFailedException e) { + assertTrue(e.getCause() instanceof CanceledFailure); } } @@ -3490,10 +3589,14 @@ public void testChildWorkflowAsyncRetry() { client.execute(taskList); fail("unreachable"); } catch (WorkflowException e) { - assertTrue( - String.valueOf(e.getCause()), e.getCause() instanceof ChildWorkflowFailureException); - assertTrue(e.getCause().getCause() instanceof UnsupportedOperationException); - assertEquals("simulated failure", e.getCause().getCause().getMessage()); + assertTrue(String.valueOf(e.getCause()), e.getCause() instanceof ChildWorkflowFailure); + assertTrue(e.getCause().getCause() instanceof ApplicationFailure); + assertEquals( + UnsupportedOperationException.class.getName(), + ((ApplicationFailure) e.getCause().getCause()).getType()); + assertEquals( + "message='simulated failure', type='java.lang.UnsupportedOperationException', nonRetryable=false", + e.getCause().getCause().getMessage()); } assertEquals(3, angryChildActivity.getInvocationCount()); } @@ -3606,7 +3709,10 @@ public void testWorkflowRetry() { workflowStub.execute(testName.getMethodName()); fail("unreachable"); } catch (WorkflowException e) { - assertEquals(e.toString(), "simulated 3", e.getCause().getMessage()); + assertEquals( + e.toString(), + "message='simulated 3', type='java.lang.IllegalStateException', nonRetryable=false", + e.getCause().getMessage()); } finally { long elapsed = currentTimeMillis() - start; assertTrue(String.valueOf(elapsed), elapsed >= 2000); // Ensure that retry delays the restart @@ -3624,9 +3730,9 @@ public String execute(String testName) { } int c = count.incrementAndGet(); if (c < 3) { - throw new IllegalStateException("simulated " + c); - } else { throw new IllegalArgumentException("simulated " + c); + } else { + throw new ApplicationFailure("simulated " + c, "NonRetryable"); } } } @@ -3637,7 +3743,7 @@ public void testWorkflowRetryDoNotRetryException() { RetryOptions workflowRetryOptions = RetryOptions.newBuilder() .setInitialInterval(Duration.ofSeconds(1)) - .setDoNotRetry(IllegalArgumentException.class) + .setDoNotRetry("NonRetryable") .setMaximumAttempts(100) .setBackoffCoefficient(1.0) .build(); @@ -3649,8 +3755,54 @@ public void testWorkflowRetryDoNotRetryException() { workflowStub.execute(testName.getMethodName()); fail("unreachable"); } catch (WorkflowException e) { - assertTrue(e.getCause() instanceof IllegalArgumentException); - assertEquals("simulated 3", e.getCause().getMessage()); + assertTrue(e.getCause() instanceof ApplicationFailure); + assertEquals("NonRetryable", ((ApplicationFailure) e.getCause()).getType()); + assertEquals( + "message='simulated 3', type='NonRetryable', nonRetryable=false", + e.getCause().getMessage()); + } + } + + public static class TestWorkflowNonRetryableFlag implements TestWorkflowRetry { + + @Override + public String execute(String testName) { + AtomicInteger count = retryCount.get(testName); + if (count == null) { + count = new AtomicInteger(); + retryCount.put(testName, count); + } + int c = count.incrementAndGet(); + ApplicationFailure f = new ApplicationFailure("simulated " + c, "foo", "details"); + if (c == 3) { + f.setNonRetryable(true); + } + throw f; + } + } + + @Test + public void testWorkflowFailureNonRetryableFlag() { + startWorkerFor(TestWorkflowNonRetryableFlag.class); + RetryOptions workflowRetryOptions = + RetryOptions.newBuilder() + .setInitialInterval(Duration.ofSeconds(1)) + .setMaximumAttempts(100) + .setBackoffCoefficient(1.0) + .build(); + TestWorkflowRetry workflowStub = + workflowClient.newWorkflowStub( + TestWorkflowRetry.class, + newWorkflowOptionsBuilder(taskList).setRetryOptions(workflowRetryOptions).build()); + try { + workflowStub.execute(testName.getMethodName()); + fail("unreachable"); + } catch (WorkflowException e) { + assertTrue(e.getCause() instanceof ApplicationFailure); + assertEquals("foo", ((ApplicationFailure) e.getCause()).getType()); + assertEquals("details", ((ApplicationFailure) e.getCause()).getDetails().get(String.class)); + assertEquals( + "message='simulated 3', type='foo', nonRetryable=true", e.getCause().getMessage()); } } @@ -3662,7 +3814,7 @@ public interface TestWorkflowRetryWithMethodRetry { initialIntervalSeconds = 1, maximumIntervalSeconds = 1, maximumAttempts = 30, - doNotRetry = IllegalArgumentException.class) + doNotRetry = "java.lang.IllegalArgumentException") String execute(String testName); } @@ -3695,8 +3847,12 @@ public void testWorkflowRetryWithMethodRetryDoNotRetryException() { workflowStub.execute(testName.getMethodName()); fail("unreachable"); } catch (WorkflowException e) { - assertTrue(e.getCause().toString(), e.getCause() instanceof IllegalArgumentException); - assertEquals("simulated 3", e.getCause().getMessage()); + assertTrue(e.getCause() instanceof ApplicationFailure); + assertEquals( + IllegalArgumentException.class.getName(), ((ApplicationFailure) e.getCause()).getType()); + assertEquals( + "message='simulated 3', type='java.lang.IllegalArgumentException', nonRetryable=false", + e.getCause().getMessage()); } } @@ -3761,7 +3917,7 @@ public void testWorkflowWithCronSchedule() { try { client.getResult(String.class); fail("unreachable"); - } catch (CancellationException ignored) { + } catch (CanceledFailure ignored) { } // Run 3 failed. So on run 4 we get the last completion result from run 2. @@ -3800,7 +3956,7 @@ public void testChildWorkflowWithCronSchedule() { try { client.getResult(String.class); fail("unreachable"); - } catch (CancellationException ignored) { + } catch (CanceledFailure ignored) { } // Run 3 failed. So on run 4 we get the last completion result from run 2. @@ -3847,6 +4003,8 @@ public interface TestActivities { void throwIO(); + void throwApplicationFailureThreeTimes(); + void neverComplete(); @MethodRetry(initialIntervalSeconds = 1, maximumIntervalSeconds = 1, maximumAttempts = 3) @@ -3864,6 +4022,7 @@ private static class TestActivitiesImpl implements TestActivities { private final ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 100, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); int lastAttempt; + final AtomicInteger applicationFailureCounter = new AtomicInteger(); private TestActivitiesImpl(ActivityCompletionClient completionClient) { this.completionClient = completionClient; @@ -4044,6 +4203,13 @@ public void throwIO() { } } + @Override + public void throwApplicationFailureThreeTimes() { + ApplicationFailure failure = new ApplicationFailure("simulated", "simulatedType"); + failure.setNonRetryable(applicationFailureCounter.incrementAndGet() > 2); + throw failure; + } + @Override public void neverComplete() { invocations.add("neverComplete"); @@ -4833,7 +4999,9 @@ public void testVersionNotSupported() { workflowStub.execute(taskList); fail("unreachable"); } catch (WorkflowException e) { - assertEquals("unsupported change version", e.getCause().getMessage()); + assertEquals( + "message='unsupported change version', type='java.lang.Exception', nonRetryable=false", + e.getCause().getMessage()); } } @@ -4869,7 +5037,7 @@ public void testNonDeterministicWorkflowPolicyBlockWorkflow() { try { workflowStub.execute(taskList); fail("unreachable"); - } catch (WorkflowTimedOutException e) { + } catch (WorkflowException e) { // expected to timeout as workflow is going get blocked. } @@ -4908,9 +5076,12 @@ public void testNonDeterministicWorkflowPolicyFailWorkflow() { try { workflowStub.execute(taskList); fail("unreachable"); - } catch (WorkflowFailureException e) { + } catch (WorkflowFailedException e) { // expected to fail on non deterministic error - assertTrue(e.getCause() instanceof Error); + assertTrue(e.getCause() instanceof ApplicationFailure); + assertEquals( + "io.temporal.internal.replay.NonDeterminisicWorkflowError", + ((ApplicationFailure) e.getCause()).getType()); String causeMsg = e.getCause().getMessage(); assertTrue(causeMsg, causeMsg.contains("nondeterministic")); } @@ -5125,7 +5296,7 @@ public String execute(String taskList) { .build()); try { activity.execute(); - } catch (ActivityFailureException e) { + } catch (ActivityFailure e) { return e.getCause().getMessage(); } return "done"; @@ -5173,14 +5344,14 @@ public String execute(String taskList) { .build()); try { activity.execute("execute", Void.class, "boo"); - } catch (ActivityFailureException e) { + } catch (ActivityFailure e) { result.append(e.getCause().getClass().getSimpleName()); } result.append("-"); try { localActivity.execute("execute", Void.class, "boo"); - } catch (ActivityFailureException e) { - result.append(e.getCause().getClass().getSimpleName()); + } catch (ActivityFailure e) { + result.append(((ApplicationFailure) e.getCause()).getType()); } return result.toString(); } @@ -5195,7 +5366,7 @@ public void testNonSerializableArgumentsInActivity() { TestWorkflow1.class, newWorkflowOptionsBuilder(taskList).build()); String result = workflowStub.execute(taskList); - assertEquals("DataConverterException-DataConverterException", result); + assertEquals("ApplicationFailure-io.temporal.common.converter.DataConverterException", result); } @WorkflowInterface @@ -5222,7 +5393,7 @@ public String execute(String taskList) { Workflow.newChildWorkflowStub(NonSerializableExceptionChildWorkflow.class); try { child.execute(taskList); - } catch (ChildWorkflowFailureException e) { + } catch (ChildWorkflowFailure e) { return e.getMessage(); } return "done"; @@ -5340,11 +5511,15 @@ public String execute(String taskList) { Workflow.newLocalActivityStub(TestActivities.class, newLocalActivityOptions1()); try { localActivities.throwIO(); - } catch (ActivityFailureException e) { + } catch (ActivityFailure e) { + e.printStackTrace(); try { assertTrue(e.getMessage().contains("throwIO")); - assertTrue(e.getCause() instanceof IOException); - assertEquals("simulated IO problem", e.getCause().getMessage()); + assertTrue(e.getCause() instanceof ApplicationFailure); + assertEquals(IOException.class.getName(), ((ApplicationFailure) e.getCause()).getType()); + assertEquals( + "message='simulated IO problem', type='java.io.IOException', nonRetryable=false", + e.getCause().getMessage()); } catch (AssertionError ae) { // Errors cause decision to fail. But we want workflow to fail in this case. throw new RuntimeException(ae); @@ -5789,7 +5964,7 @@ public String execute(String taskList, String keyword) { searchAttributes = Workflow.getWorkflowInfo().getSearchAttributes(); assertEquals( "testKey", - WorkflowUtils.getValueFromSearchAttributes( + SearchAttributesUtil.getValueFromSearchAttributes( searchAttributes, "CustomKeywordField", String.class)); // Running the activity below ensures that we have one more decision task to be executed after @@ -6007,7 +6182,7 @@ public void testSignalAndQueryListener() { signalStub.getSignal(); fail("unreachable"); // as not listener is not registered yet } catch (WorkflowQueryException e) { - assertTrue(e.getMessage().contains("Unknown query type: getSignal")); + assertTrue(e.getCause().getMessage().contains("Unknown query type: getSignal")); } stub.register(); while (true) { diff --git a/src/test/resources/testAsyncActivityRetryHistory.json b/src/test/resources/testAsyncActivityRetryHistory.json index c45d03a234..acc44164f0 100755 --- a/src/test/resources/testAsyncActivityRetryHistory.json +++ b/src/test/resources/testAsyncActivityRetryHistory.json @@ -1,60 +1,60 @@ { "events": [{ "eventId": "1", - "timestamp": "1590453492236550100", + "timestamp": "1591983071571562000", "version": "-24", - "taskId": "1060588", + "taskId": "2111352", "workflowExecutionStartedEventAttributes": { "workflowType": { "name": "TestWorkflow1" }, "taskList": { - "name": "WorkflowTest-testAsyncActivityRetry-43fa68b9-f085-43ef-88b7-b7e090e6e733" + "name": "WorkflowTest-testAsyncActivityRetry-1b2daed8-7638-4186-86ce-f84de3e4efc5" }, "input": { "payloads": [{ "metadata": { "encoding": "anNvbg==" }, - "data": "IldvcmtmbG93VGVzdC10ZXN0QXN5bmNBY3Rpdml0eVJldHJ5LTQzZmE2OGI5LWYwODUtNDNlZi04OGI3LWI3ZTA5MGU2ZTczMyI=" + "data": "IldvcmtmbG93VGVzdC10ZXN0QXN5bmNBY3Rpdml0eVJldHJ5LTFiMmRhZWQ4LTc2MzgtNDE4Ni04NmNlLWY4NGRlM2U0ZWZjNSI=" }] }, "workflowExecutionTimeoutSeconds": 315360000, "workflowRunTimeoutSeconds": 108000, "workflowTaskTimeoutSeconds": 5, - "originalExecutionRunId": "19ef0dad-e38b-451c-a23e-c20b82128d56", + "originalExecutionRunId": "f78f8ebe-637e-4acd-bbfd-679408e465cd", "identity": "unknown-mac", - "firstExecutionRunId": "19ef0dad-e38b-451c-a23e-c20b82128d56" + "firstExecutionRunId": "f78f8ebe-637e-4acd-bbfd-679408e465cd" } }, { "eventId": "2", - "timestamp": "1590453492236608200", + "timestamp": "1591983071571659600", "eventType": "DecisionTaskScheduled", "version": "-24", - "taskId": "1060589", + "taskId": "2111353", "decisionTaskScheduledEventAttributes": { "taskList": { - "name": "WorkflowTest-testAsyncActivityRetry-43fa68b9-f085-43ef-88b7-b7e090e6e733" + "name": "WorkflowTest-testAsyncActivityRetry-1b2daed8-7638-4186-86ce-f84de3e4efc5" }, "startToCloseTimeoutSeconds": 5 } }, { "eventId": "3", - "timestamp": "1590453492246700500", + "timestamp": "1591983071583733900", "eventType": "DecisionTaskStarted", "version": "-24", - "taskId": "1060594", + "taskId": "2111358", "decisionTaskStartedEventAttributes": { "scheduledEventId": "2", "identity": "unknown-mac", - "requestId": "e4c732aa-6f15-413d-9d15-c5abf29feeca" + "requestId": "94c2da84-3306-4f30-b019-f62aac03d709" } }, { "eventId": "4", - "timestamp": "1590453492257027600", + "timestamp": "1591983071594495200", "eventType": "DecisionTaskCompleted", "version": "-24", - "taskId": "1060597", + "taskId": "2111361", "decisionTaskCompletedEventAttributes": { "scheduledEventId": "2", "startedEventId": "3", @@ -62,17 +62,17 @@ } }, { "eventId": "5", - "timestamp": "1590453492257079000", + "timestamp": "1591983071594527600", "eventType": "ActivityTaskScheduled", "version": "-24", - "taskId": "1060598", + "taskId": "2111362", "activityTaskScheduledEventAttributes": { "activityId": "0", "activityType": { "name": "heartbeatAndThrowIO" }, "taskList": { - "name": "WorkflowTest-testAsyncActivityRetry-43fa68b9-f085-43ef-88b7-b7e090e6e733" + "name": "WorkflowTest-testAsyncActivityRetry-1b2daed8-7638-4186-86ce-f84de3e4efc5" }, "scheduleToCloseTimeoutSeconds": 5, "scheduleToStartTimeoutSeconds": 5, @@ -88,74 +88,73 @@ } }, { "eventId": "6", - "timestamp": "1590453494307545100", + "timestamp": "1591983073651924100", "eventType": "ActivityTaskStarted", "version": "-24", - "taskId": "1060609", + "taskId": "2111373", "activityTaskStartedEventAttributes": { "scheduledEventId": "5", "identity": "unknown-mac", - "requestId": "7e9cf5d6-dee9-4c3e-bd1c-c688e5fded94", + "requestId": "62e30270-d480-42c4-a08a-dc7eab180e0f", "attempt": 2, - "lastFailureReason": "java.io.IOException", - "lastFailureDetails": { - "payloads": [{ - "metadata": { - "encoding": "anNvbg==" - }, - "data": "eyJkZXRhaWxNZXNzYWdlIjoic2ltdWxhdGVkIElPIHByb2JsZW0iLCJzdGFja1RyYWNlIjoiaW8udGVtcG9yYWwud29ya2Zsb3cuV29ya2Zsb3dUZXN0JFRlc3RBY3Rpdml0aWVzSW1wbC5oZWFydGJlYXRBbmRUaHJvd0lPKFdvcmtmbG93VGVzdC5qYXZhOjQwMjQpXG5zdW4ucmVmbGVjdC5OYXRpdmVNZXRob2RBY2Nlc3NvckltcGwuaW52b2tlMChOYXRpdmUgTWV0aG9kKVxuc3VuLnJlZmxlY3QuTmF0aXZlTWV0aG9kQWNjZXNzb3JJbXBsLmludm9rZShOYXRpdmVNZXRob2RBY2Nlc3NvckltcGwuamF2YTo2MilcbnN1bi5yZWZsZWN0LkRlbGVnYXRpbmdNZXRob2RBY2Nlc3NvckltcGwuaW52b2tlKERlbGVnYXRpbmdNZXRob2RBY2Nlc3NvckltcGwuamF2YTo0MylcbmphdmEubGFuZy5yZWZsZWN0Lk1ldGhvZC5pbnZva2UoTWV0aG9kLmphdmE6NDk4KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwuc3luYy5QT0pPQWN0aXZpdHlUYXNrSGFuZGxlciRQT0pPQWN0aXZpdHlJbXBsZW1lbnRhdGlvbi5leGVjdXRlKFBPSk9BY3Rpdml0eVRhc2tIYW5kbGVyLmphdmE6MjA3KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwuc3luYy5QT0pPQWN0aXZpdHlUYXNrSGFuZGxlci5oYW5kbGUoUE9KT0FjdGl2aXR5VGFza0hhbmRsZXIuamF2YToxODApXG5pby50ZW1wb3JhbC5pbnRlcm5hbC53b3JrZXIuQWN0aXZpdHlXb3JrZXIkVGFza0hhbmRsZXJJbXBsLmhhbmRsZShBY3Rpdml0eVdvcmtlci5qYXZhOjE4MylcbmlvLnRlbXBvcmFsLmludGVybmFsLndvcmtlci5BY3Rpdml0eVdvcmtlciRUYXNrSGFuZGxlckltcGwuaGFuZGxlKEFjdGl2aXR5V29ya2VyLmphdmE6MTQ2KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwud29ya2VyLlBvbGxUYXNrRXhlY3V0b3IubGFtYmRhJHByb2Nlc3MkMChQb2xsVGFza0V4ZWN1dG9yLmphdmE6NzMpXG5qYXZhLnV0aWwuY29uY3VycmVudC5UaHJlYWRQb29sRXhlY3V0b3IucnVuV29ya2VyKFRocmVhZFBvb2xFeGVjdXRvci5qYXZhOjExNDkpXG5qYXZhLnV0aWwuY29uY3VycmVudC5UaHJlYWRQb29sRXhlY3V0b3IkV29ya2VyLnJ1bihUaHJlYWRQb29sRXhlY3V0b3IuamF2YTo2MjQpXG5qYXZhLmxhbmcuVGhyZWFkLnJ1bihUaHJlYWQuamF2YTo3NDgpXG4iLCJzdXBwcmVzc2VkRXhjZXB0aW9ucyI6W10sImNsYXNzIjoiamF2YS5pby5JT0V4Y2VwdGlvbiJ9" - }] + "lastFailure": { + "message": "simulated IO problem", + "source": "JavaSDK", + "stackTrace": "io.temporal.workflow.WorkflowTest$TestActivitiesImpl.heartbeatAndThrowIO(WorkflowTest.java:4103)\nsun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\nsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\nsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\njava.lang.reflect.Method.invoke(Method.java:498)\nio.temporal.internal.sync.POJOActivityTaskHandler$POJOActivityImplementation.execute(POJOActivityTaskHandler.java:210)\nio.temporal.internal.sync.POJOActivityTaskHandler.handle(POJOActivityTaskHandler.java:183)\nio.temporal.internal.worker.ActivityWorker$TaskHandlerImpl.handle(ActivityWorker.java:183)\nio.temporal.internal.worker.ActivityWorker$TaskHandlerImpl.handle(ActivityWorker.java:146)\nio.temporal.internal.worker.PollTaskExecutor.lambda$process$0(PollTaskExecutor.java:73)\njava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\njava.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\njava.lang.Thread.run(Thread.java:748)\n", + "applicationFailureInfo": { + "type": "java.io.IOException" + } } } }, { "eventId": "7", - "timestamp": "1590453494325340900", + "timestamp": "1591983073667901500", "eventType": "ActivityTaskFailed", "version": "-24", - "taskId": "1060610", + "taskId": "2111374", "activityTaskFailedEventAttributes": { - "reason": "java.io.IOException", - "details": { - "payloads": [{ - "metadata": { - "encoding": "anNvbg==" - }, - "data": "eyJkZXRhaWxNZXNzYWdlIjoic2ltdWxhdGVkIElPIHByb2JsZW0iLCJzdGFja1RyYWNlIjoiaW8udGVtcG9yYWwud29ya2Zsb3cuV29ya2Zsb3dUZXN0JFRlc3RBY3Rpdml0aWVzSW1wbC5oZWFydGJlYXRBbmRUaHJvd0lPKFdvcmtmbG93VGVzdC5qYXZhOjQwMjQpXG5zdW4ucmVmbGVjdC5OYXRpdmVNZXRob2RBY2Nlc3NvckltcGwuaW52b2tlMChOYXRpdmUgTWV0aG9kKVxuc3VuLnJlZmxlY3QuTmF0aXZlTWV0aG9kQWNjZXNzb3JJbXBsLmludm9rZShOYXRpdmVNZXRob2RBY2Nlc3NvckltcGwuamF2YTo2MilcbnN1bi5yZWZsZWN0LkRlbGVnYXRpbmdNZXRob2RBY2Nlc3NvckltcGwuaW52b2tlKERlbGVnYXRpbmdNZXRob2RBY2Nlc3NvckltcGwuamF2YTo0MylcbmphdmEubGFuZy5yZWZsZWN0Lk1ldGhvZC5pbnZva2UoTWV0aG9kLmphdmE6NDk4KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwuc3luYy5QT0pPQWN0aXZpdHlUYXNrSGFuZGxlciRQT0pPQWN0aXZpdHlJbXBsZW1lbnRhdGlvbi5leGVjdXRlKFBPSk9BY3Rpdml0eVRhc2tIYW5kbGVyLmphdmE6MjA3KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwuc3luYy5QT0pPQWN0aXZpdHlUYXNrSGFuZGxlci5oYW5kbGUoUE9KT0FjdGl2aXR5VGFza0hhbmRsZXIuamF2YToxODApXG5pby50ZW1wb3JhbC5pbnRlcm5hbC53b3JrZXIuQWN0aXZpdHlXb3JrZXIkVGFza0hhbmRsZXJJbXBsLmhhbmRsZShBY3Rpdml0eVdvcmtlci5qYXZhOjE4MylcbmlvLnRlbXBvcmFsLmludGVybmFsLndvcmtlci5BY3Rpdml0eVdvcmtlciRUYXNrSGFuZGxlckltcGwuaGFuZGxlKEFjdGl2aXR5V29ya2VyLmphdmE6MTQ2KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwud29ya2VyLlBvbGxUYXNrRXhlY3V0b3IubGFtYmRhJHByb2Nlc3MkMChQb2xsVGFza0V4ZWN1dG9yLmphdmE6NzMpXG5qYXZhLnV0aWwuY29uY3VycmVudC5UaHJlYWRQb29sRXhlY3V0b3IucnVuV29ya2VyKFRocmVhZFBvb2xFeGVjdXRvci5qYXZhOjExNDkpXG5qYXZhLnV0aWwuY29uY3VycmVudC5UaHJlYWRQb29sRXhlY3V0b3IkV29ya2VyLnJ1bihUaHJlYWRQb29sRXhlY3V0b3IuamF2YTo2MjQpXG5qYXZhLmxhbmcuVGhyZWFkLnJ1bihUaHJlYWQuamF2YTo3NDgpXG4iLCJzdXBwcmVzc2VkRXhjZXB0aW9ucyI6W10sImNsYXNzIjoiamF2YS5pby5JT0V4Y2VwdGlvbiJ9" - }] + "failure": { + "message": "simulated IO problem", + "source": "JavaSDK", + "stackTrace": "io.temporal.workflow.WorkflowTest$TestActivitiesImpl.heartbeatAndThrowIO(WorkflowTest.java:4103)\nsun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\nsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\nsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\njava.lang.reflect.Method.invoke(Method.java:498)\nio.temporal.internal.sync.POJOActivityTaskHandler$POJOActivityImplementation.execute(POJOActivityTaskHandler.java:210)\nio.temporal.internal.sync.POJOActivityTaskHandler.handle(POJOActivityTaskHandler.java:183)\nio.temporal.internal.worker.ActivityWorker$TaskHandlerImpl.handle(ActivityWorker.java:183)\nio.temporal.internal.worker.ActivityWorker$TaskHandlerImpl.handle(ActivityWorker.java:146)\nio.temporal.internal.worker.PollTaskExecutor.lambda$process$0(PollTaskExecutor.java:73)\njava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\njava.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\njava.lang.Thread.run(Thread.java:748)\n", + "applicationFailureInfo": { + "type": "java.io.IOException" + } }, "scheduledEventId": "5", "startedEventId": "6", - "identity": "unknown-mac" + "identity": "unknown-mac", + "retryStatus": "MaximumAttemptsReached" } }, { "eventId": "8", - "timestamp": "1590453494325386600", + "timestamp": "1591983073667969400", "eventType": "DecisionTaskScheduled", "version": "-24", - "taskId": "1060613", + "taskId": "2111377", "decisionTaskScheduledEventAttributes": { "taskList": { - "name": "unknown-mac:4dfcddaa-68a1-48af-8191-37785df06155" + "name": "unknown-mac:d10a86f0-842b-4d15-a9e9-81dbddc91b24" }, "startToCloseTimeoutSeconds": 5 } }, { "eventId": "9", - "timestamp": "1590453494334613200", + "timestamp": "1591983073675883300", "eventType": "DecisionTaskStarted", "version": "-24", - "taskId": "1060617", + "taskId": "2111381", "decisionTaskStartedEventAttributes": { "scheduledEventId": "8", - "identity": "4dfcddaa-68a1-48af-8191-37785df06155", - "requestId": "9adee790-782c-4b13-a025-d5b93a5c2243" + "identity": "d10a86f0-842b-4d15-a9e9-81dbddc91b24", + "requestId": "1004749c-9a0f-4f1d-94bd-7a8ccb69c7fe" } }, { "eventId": "10", - "timestamp": "1590453494346994200", + "timestamp": "1591983073686059300", "eventType": "DecisionTaskCompleted", "version": "-24", - "taskId": "1060620", + "taskId": "2111384", "decisionTaskCompletedEventAttributes": { "scheduledEventId": "8", "startedEventId": "9", @@ -163,20 +162,32 @@ } }, { "eventId": "11", - "timestamp": "1590453494347026800", + "timestamp": "1591983073686088500", "eventType": "WorkflowExecutionFailed", "version": "-24", - "taskId": "1060621", + "taskId": "2111385", "workflowExecutionFailedEventAttributes": { - "reason": "io.temporal.workflow.ActivityFailureException", - "details": { - "payloads": [{ - "metadata": { - "encoding": "anNvbg==" + "failure": { + "source": "JavaSDK", + "stackTrace": "java.lang.Thread.getStackTrace(Thread.java:1559)\nio.temporal.internal.sync.CompletablePromiseImpl.throwFailure(CompletablePromiseImpl.java:135)\nio.temporal.internal.sync.CompletablePromiseImpl.getImpl(CompletablePromiseImpl.java:94)\nio.temporal.internal.sync.CompletablePromiseImpl.get(CompletablePromiseImpl.java:73)\nio.temporal.workflow.WorkflowTest$TestAsyncActivityRetry.execute(WorkflowTest.java:849)\nsun.reflect.GeneratedMethodAccessor10.invoke(Unknown Source)\nsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\njava.lang.reflect.Method.invoke(Method.java:498)\nio.temporal.internal.sync.POJOWorkflowImplementationFactory$POJOWorkflowImplementation$RootWorkflowInvocationInterceptor.execute(POJOWorkflowImplementationFactory.java:284)\nio.temporal.common.interceptors.BaseWorkflowInvoker.execute(BaseWorkflowInvoker.java:39)\nio.temporal.internal.sync.POJOWorkflowImplementationFactory$POJOWorkflowImplementation.execute(POJOWorkflowImplementationFactory.java:248)\nio.temporal.internal.sync.WorkflowExecuteRunnable.run(WorkflowExecuteRunnable.java:52)\nio.temporal.internal.sync.SyncWorkflow.lambda$start$0(SyncWorkflow.java:118)\nio.temporal.internal.sync.CancellationScopeImpl.run(CancellationScopeImpl.java:104)\nio.temporal.internal.sync.WorkflowThreadImpl$RunnableWrapper.run(WorkflowThreadImpl.java:106)\njava.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)\njava.util.concurrent.FutureTask.run(FutureTask.java:266)\njava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\njava.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\njava.lang.Thread.run(Thread.java:748)\n", + "cause": { + "message": "simulated IO problem", + "source": "JavaSDK", + "stackTrace": "io.temporal.workflow.WorkflowTest$TestActivitiesImpl.heartbeatAndThrowIO(WorkflowTest.java:4103)\nsun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\nsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\nsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\njava.lang.reflect.Method.invoke(Method.java:498)\nio.temporal.internal.sync.POJOActivityTaskHandler$POJOActivityImplementation.execute(POJOActivityTaskHandler.java:210)\nio.temporal.internal.sync.POJOActivityTaskHandler.handle(POJOActivityTaskHandler.java:183)\nio.temporal.internal.worker.ActivityWorker$TaskHandlerImpl.handle(ActivityWorker.java:183)\nio.temporal.internal.worker.ActivityWorker$TaskHandlerImpl.handle(ActivityWorker.java:146)\nio.temporal.internal.worker.PollTaskExecutor.lambda$process$0(PollTaskExecutor.java:73)\njava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\njava.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\njava.lang.Thread.run(Thread.java:748)\n", + "applicationFailureInfo": { + "type": "java.io.IOException" + } + }, + "activityFailureInfo": { + "scheduledEventId": "5", + "startedEventId": "6", + "activityType": { + "name": "heartbeatAndThrowIO" }, - "data": "eyJhdHRlbXB0IjowLCJiYWNrb2ZmIjpudWxsLCJhY3Rpdml0eVR5cGUiOnsibmFtZV8iOiJoZWFydGJlYXRBbmRUaHJvd0lPIiwibWVtb2l6ZWRJc0luaXRpYWxpemVkIjoxLCJ1bmtub3duRmllbGRzIjp7ImZpZWxkcyI6e30sImZpZWxkc0Rlc2NlbmRpbmciOnt9fSwibWVtb2l6ZWRTaXplIjoyMSwibWVtb2l6ZWRIYXNoQ29kZSI6MH0sImFjdGl2aXR5SWQiOm51bGwsImV2ZW50SWQiOjcsImRldGFpbE1lc3NhZ2UiOiJBY3Rpdml0eUZhaWx1cmVFeGNlcHRpb24sIEFjdGl2aXR5VHlwZVx1MDAzZFwiaGVhcnRiZWF0QW5kVGhyb3dJT1wiLCBBY3Rpdml0eUlkXHUwMDNkXCJudWxsXCIsIEV2ZW50SWRcdTAwM2Q3IiwiY2F1c2UiOnsiZGV0YWlsTWVzc2FnZSI6InNpbXVsYXRlZCBJTyBwcm9ibGVtIiwic3RhY2tUcmFjZSI6ImlvLnRlbXBvcmFsLndvcmtmbG93LldvcmtmbG93VGVzdCRUZXN0QWN0aXZpdGllc0ltcGwuaGVhcnRiZWF0QW5kVGhyb3dJTyhXb3JrZmxvd1Rlc3QuamF2YTo0MDI0KVxuc3VuLnJlZmxlY3QuTmF0aXZlTWV0aG9kQWNjZXNzb3JJbXBsLmludm9rZTAoTmF0aXZlIE1ldGhvZDowKVxuc3VuLnJlZmxlY3QuTmF0aXZlTWV0aG9kQWNjZXNzb3JJbXBsLmludm9rZShOYXRpdmVNZXRob2RBY2Nlc3NvckltcGwuamF2YTo2MilcbnN1bi5yZWZsZWN0LkRlbGVnYXRpbmdNZXRob2RBY2Nlc3NvckltcGwuaW52b2tlKERlbGVnYXRpbmdNZXRob2RBY2Nlc3NvckltcGwuamF2YTo0MylcbmphdmEubGFuZy5yZWZsZWN0Lk1ldGhvZC5pbnZva2UoTWV0aG9kLmphdmE6NDk4KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwuc3luYy5QT0pPQWN0aXZpdHlUYXNrSGFuZGxlciRQT0pPQWN0aXZpdHlJbXBsZW1lbnRhdGlvbi5leGVjdXRlKFBPSk9BY3Rpdml0eVRhc2tIYW5kbGVyLmphdmE6MjA3KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwuc3luYy5QT0pPQWN0aXZpdHlUYXNrSGFuZGxlci5oYW5kbGUoUE9KT0FjdGl2aXR5VGFza0hhbmRsZXIuamF2YToxODApXG5pby50ZW1wb3JhbC5pbnRlcm5hbC53b3JrZXIuQWN0aXZpdHlXb3JrZXIkVGFza0hhbmRsZXJJbXBsLmhhbmRsZShBY3Rpdml0eVdvcmtlci5qYXZhOjE4MylcbmlvLnRlbXBvcmFsLmludGVybmFsLndvcmtlci5BY3Rpdml0eVdvcmtlciRUYXNrSGFuZGxlckltcGwuaGFuZGxlKEFjdGl2aXR5V29ya2VyLmphdmE6MTQ2KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwud29ya2VyLlBvbGxUYXNrRXhlY3V0b3IubGFtYmRhJHByb2Nlc3MkMChQb2xsVGFza0V4ZWN1dG9yLmphdmE6NzMpXG5qYXZhLnV0aWwuY29uY3VycmVudC5UaHJlYWRQb29sRXhlY3V0b3IucnVuV29ya2VyKFRocmVhZFBvb2xFeGVjdXRvci5qYXZhOjExNDkpXG5qYXZhLnV0aWwuY29uY3VycmVudC5UaHJlYWRQb29sRXhlY3V0b3IkV29ya2VyLnJ1bihUaHJlYWRQb29sRXhlY3V0b3IuamF2YTo2MjQpXG5qYXZhLmxhbmcuVGhyZWFkLnJ1bihUaHJlYWQuamF2YTo3NDgpXG4iLCJzdXBwcmVzc2VkRXhjZXB0aW9ucyI6W10sImNsYXNzIjoiamF2YS5pby5JT0V4Y2VwdGlvbiJ9LCJzdGFja1RyYWNlIjoiamF2YS5sYW5nLlRocmVhZC5nZXRTdGFja1RyYWNlKFRocmVhZC5qYXZhOjE1NTkpXG5pby50ZW1wb3JhbC5pbnRlcm5hbC5zeW5jLkNvbXBsZXRhYmxlUHJvbWlzZUltcGwudGhyb3dGYWlsdXJlKENvbXBsZXRhYmxlUHJvbWlzZUltcGwuamF2YToxMzUpXG5pby50ZW1wb3JhbC5pbnRlcm5hbC5zeW5jLkNvbXBsZXRhYmxlUHJvbWlzZUltcGwuZ2V0SW1wbChDb21wbGV0YWJsZVByb21pc2VJbXBsLmphdmE6OTQpXG5pby50ZW1wb3JhbC5pbnRlcm5hbC5zeW5jLkNvbXBsZXRhYmxlUHJvbWlzZUltcGwuZ2V0KENvbXBsZXRhYmxlUHJvbWlzZUltcGwuamF2YTo3MylcbmlvLnRlbXBvcmFsLndvcmtmbG93LldvcmtmbG93VGVzdCRUZXN0QXN5bmNBY3Rpdml0eVJldHJ5LmV4ZWN1dGUoV29ya2Zsb3dUZXN0LmphdmE6ODMwKVxuc3VuLnJlZmxlY3QuR2VuZXJhdGVkTWV0aG9kQWNjZXNzb3IxNC5pbnZva2UoVW5rbm93biBTb3VyY2UpXG5zdW4ucmVmbGVjdC5EZWxlZ2F0aW5nTWV0aG9kQWNjZXNzb3JJbXBsLmludm9rZShEZWxlZ2F0aW5nTWV0aG9kQWNjZXNzb3JJbXBsLmphdmE6NDMpXG5qYXZhLmxhbmcucmVmbGVjdC5NZXRob2QuaW52b2tlKE1ldGhvZC5qYXZhOjQ5OClcbmlvLnRlbXBvcmFsLmludGVybmFsLnN5bmMuUE9KT1dvcmtmbG93SW1wbGVtZW50YXRpb25GYWN0b3J5JFBPSk9Xb3JrZmxvd0ltcGxlbWVudGF0aW9uJFJvb3RXb3JrZmxvd0ludm9jYXRpb25JbnRlcmNlcHRvci5leGVjdXRlKFBPSk9Xb3JrZmxvd0ltcGxlbWVudGF0aW9uRmFjdG9yeS5qYXZhOjI4MylcbmlvLnRlbXBvcmFsLmNvbW1vbi5pbnRlcmNlcHRvcnMuQmFzZVdvcmtmbG93SW52b2tlci5leGVjdXRlKEJhc2VXb3JrZmxvd0ludm9rZXIuamF2YTozOSlcbmlvLnRlbXBvcmFsLmludGVybmFsLnN5bmMuUE9KT1dvcmtmbG93SW1wbGVtZW50YXRpb25GYWN0b3J5JFBPSk9Xb3JrZmxvd0ltcGxlbWVudGF0aW9uLmV4ZWN1dGUoUE9KT1dvcmtmbG93SW1wbGVtZW50YXRpb25GYWN0b3J5LmphdmE6MjQ3KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwuc3luYy5Xb3JrZmxvd0V4ZWN1dGVSdW5uYWJsZS5ydW4oV29ya2Zsb3dFeGVjdXRlUnVubmFibGUuamF2YTo1MilcbmlvLnRlbXBvcmFsLmludGVybmFsLnN5bmMuU3luY1dvcmtmbG93LmxhbWJkYSRzdGFydCQwKFN5bmNXb3JrZmxvdy5qYXZhOjExNylcbmlvLnRlbXBvcmFsLmludGVybmFsLnN5bmMuQ2FuY2VsbGF0aW9uU2NvcGVJbXBsLnJ1bihDYW5jZWxsYXRpb25TY29wZUltcGwuamF2YToxMDQpXG5pby50ZW1wb3JhbC5pbnRlcm5hbC5zeW5jLldvcmtmbG93VGhyZWFkSW1wbCRSdW5uYWJsZVdyYXBwZXIucnVuKFdvcmtmbG93VGhyZWFkSW1wbC5qYXZhOjEwNilcbmphdmEudXRpbC5jb25jdXJyZW50LkV4ZWN1dG9ycyRSdW5uYWJsZUFkYXB0ZXIuY2FsbChFeGVjdXRvcnMuamF2YTo1MTEpXG5qYXZhLnV0aWwuY29uY3VycmVudC5GdXR1cmVUYXNrLnJ1bihGdXR1cmVUYXNrLmphdmE6MjY2KVxuamF2YS51dGlsLmNvbmN1cnJlbnQuVGhyZWFkUG9vbEV4ZWN1dG9yLnJ1bldvcmtlcihUaHJlYWRQb29sRXhlY3V0b3IuamF2YToxMTQ5KVxuamF2YS51dGlsLmNvbmN1cnJlbnQuVGhyZWFkUG9vbEV4ZWN1dG9yJFdvcmtlci5ydW4oVGhyZWFkUG9vbEV4ZWN1dG9yLmphdmE6NjI0KVxuamF2YS5sYW5nLlRocmVhZC5ydW4oVGhyZWFkLmphdmE6NzQ4KVxuIiwic3VwcHJlc3NlZEV4Y2VwdGlvbnMiOltdLCJjbGFzcyI6ImlvLnRlbXBvcmFsLndvcmtmbG93LkFjdGl2aXR5RmFpbHVyZUV4Y2VwdGlvbiJ9" - }] + "retryStatus": "Timeout" + } }, + "retryStatus": "RetryPolicyNotSet", "decisionTaskCompletedEventId": "10" } }] diff --git a/src/test/resources/testChildWorkflowRetryHistory.json b/src/test/resources/testChildWorkflowRetryHistory.json index 5fad7a5e95..8c99cd641c 100755 --- a/src/test/resources/testChildWorkflowRetryHistory.json +++ b/src/test/resources/testChildWorkflowRetryHistory.json @@ -1,60 +1,60 @@ { "events": [{ "eventId": "1", - "timestamp": "1590453586162633100", + "timestamp": "1591983166771693900", "version": "-24", - "taskId": "1061750", + "taskId": "5253128", "workflowExecutionStartedEventAttributes": { "workflowType": { "name": "TestWorkflow1" }, "taskList": { - "name": "WorkflowTest-testChildWorkflowRetry-812d408a-ac3a-46ba-aa17-08af8a4ec2ce" + "name": "WorkflowTest-testChildWorkflowRetry-4bd2d4a3-c8f7-40f2-a0b6-56c190e0aa67" }, "input": { "payloads": [{ "metadata": { "encoding": "anNvbg==" }, - "data": "IldvcmtmbG93VGVzdC10ZXN0Q2hpbGRXb3JrZmxvd1JldHJ5LTgxMmQ0MDhhLWFjM2EtNDZiYS1hYTE3LTA4YWY4YTRlYzJjZSI=" + "data": "IldvcmtmbG93VGVzdC10ZXN0Q2hpbGRXb3JrZmxvd1JldHJ5LTRiZDJkNGEzLWM4ZjctNDBmMi1hMGI2LTU2YzE5MGUwYWE2NyI=" }] }, "workflowExecutionTimeoutSeconds": 315360000, "workflowRunTimeoutSeconds": 20, "workflowTaskTimeoutSeconds": 2, - "originalExecutionRunId": "b22d99a6-d0f8-43e8-b9bf-b995e585db89", + "originalExecutionRunId": "5fcd851f-1544-4318-9dea-a5803d529af8", "identity": "unknown-mac", - "firstExecutionRunId": "b22d99a6-d0f8-43e8-b9bf-b995e585db89" + "firstExecutionRunId": "5fcd851f-1544-4318-9dea-a5803d529af8" } }, { "eventId": "2", - "timestamp": "1590453586162709900", + "timestamp": "1591983166771799700", "eventType": "DecisionTaskScheduled", "version": "-24", - "taskId": "1061751", + "taskId": "5253129", "decisionTaskScheduledEventAttributes": { "taskList": { - "name": "WorkflowTest-testChildWorkflowRetry-812d408a-ac3a-46ba-aa17-08af8a4ec2ce" + "name": "WorkflowTest-testChildWorkflowRetry-4bd2d4a3-c8f7-40f2-a0b6-56c190e0aa67" }, "startToCloseTimeoutSeconds": 2 } }, { "eventId": "3", - "timestamp": "1590453586173062700", + "timestamp": "1591983166785737100", "eventType": "DecisionTaskStarted", "version": "-24", - "taskId": "1061756", + "taskId": "5253134", "decisionTaskStartedEventAttributes": { "scheduledEventId": "2", "identity": "unknown-mac", - "requestId": "efc0a556-9220-4b01-b817-7bc886736e49" + "requestId": "f9f45099-30b7-47c1-aaae-9d28a2a525d7" } }, { "eventId": "4", - "timestamp": "1590453586182568700", + "timestamp": "1591983166795781200", "eventType": "DecisionTaskCompleted", "version": "-24", - "taskId": "1061759", + "taskId": "5253137", "decisionTaskCompletedEventAttributes": { "scheduledEventId": "2", "startedEventId": "3", @@ -62,24 +62,24 @@ } }, { "eventId": "5", - "timestamp": "1590453586182600800", + "timestamp": "1591983166795815100", "eventType": "StartChildWorkflowExecutionInitiated", "version": "-24", - "taskId": "1061760", + "taskId": "5253138", "startChildWorkflowExecutionInitiatedEventAttributes": { - "workflowId": "0d162baa-a415-34b0-8f85-6fe5638f32cc", + "workflowId": "8ac3a14e-a0fb-3c8a-881c-96f18d8ed768", "workflowType": { "name": "ITestChild" }, "taskList": { - "name": "WorkflowTest-testChildWorkflowRetry-812d408a-ac3a-46ba-aa17-08af8a4ec2ce" + "name": "WorkflowTest-testChildWorkflowRetry-4bd2d4a3-c8f7-40f2-a0b6-56c190e0aa67" }, "input": { "payloads": [{ "metadata": { "encoding": "anNvbg==" }, - "data": "IldvcmtmbG93VGVzdC10ZXN0Q2hpbGRXb3JrZmxvd1JldHJ5LTgxMmQ0MDhhLWFjM2EtNDZiYS1hYTE3LTA4YWY4YTRlYzJjZSI=" + "data": "IldvcmtmbG93VGVzdC10ZXN0Q2hpbGRXb3JrZmxvd1JldHJ5LTRiZDJkNGEzLWM4ZjctNDBmMi1hMGI2LTU2YzE5MGUwYWE2NyI=" }, { "metadata": { "encoding": "anNvbg==" @@ -100,15 +100,15 @@ } }, { "eventId": "6", - "timestamp": "1590453586193316600", + "timestamp": "1591983166808420100", "eventType": "ChildWorkflowExecutionStarted", "version": "-24", - "taskId": "1061763", + "taskId": "5253141", "childWorkflowExecutionStartedEventAttributes": { "initiatedEventId": "5", "workflowExecution": { - "workflowId": "0d162baa-a415-34b0-8f85-6fe5638f32cc", - "runId": "ff4009b6-30c0-4f30-a3d5-24b0db595ba2" + "workflowId": "8ac3a14e-a0fb-3c8a-881c-96f18d8ed768", + "runId": "ea660af5-691b-40f8-b9f7-48d22088e109" }, "workflowType": { "name": "ITestChild" @@ -116,33 +116,33 @@ } }, { "eventId": "7", - "timestamp": "1590453586193360100", + "timestamp": "1591983166808466200", "eventType": "DecisionTaskScheduled", "version": "-24", - "taskId": "1061765", + "taskId": "5253143", "decisionTaskScheduledEventAttributes": { "taskList": { - "name": "unknown-mac:834bd77a-77e9-4209-9029-95eba1365b1c" + "name": "unknown-mac:bb59cb91-cbd7-482d-bc86-9b662f0d0127" }, "startToCloseTimeoutSeconds": 2 } }, { "eventId": "8", - "timestamp": "1590453586203731500", + "timestamp": "1591983166821494900", "eventType": "DecisionTaskStarted", "version": "-24", - "taskId": "1061769", + "taskId": "5253147", "decisionTaskStartedEventAttributes": { "scheduledEventId": "7", - "identity": "834bd77a-77e9-4209-9029-95eba1365b1c", - "requestId": "f810bf4a-1f03-40fb-b0bd-fecd59f527c9" + "identity": "bb59cb91-cbd7-482d-bc86-9b662f0d0127", + "requestId": "fa1cdb44-6947-4fd3-90bc-2f7205d61e6e" } }, { "eventId": "9", - "timestamp": "1590453586211729900", + "timestamp": "1591983166831250000", "eventType": "DecisionTaskCompleted", "version": "-24", - "taskId": "1061772", + "taskId": "5253150", "decisionTaskCompletedEventAttributes": { "scheduledEventId": "7", "startedEventId": "8", @@ -150,59 +150,59 @@ } }, { "eventId": "10", - "timestamp": "1590453588355236100", + "timestamp": "1591983168978736700", "eventType": "ChildWorkflowExecutionFailed", "version": "-24", - "taskId": "1061774", + "taskId": "5253152", "childWorkflowExecutionFailedEventAttributes": { - "reason": "java.lang.UnsupportedOperationException", - "details": { - "payloads": [{ - "metadata": { - "encoding": "anNvbg==" - }, - "data": "eyJkZXRhaWxNZXNzYWdlIjoic2ltdWxhdGVkIGZhaWx1cmUiLCJzdGFja1RyYWNlIjoiaW8udGVtcG9yYWwud29ya2Zsb3cuV29ya2Zsb3dUZXN0JEFuZ3J5Q2hpbGQuZXhlY3V0ZShXb3JrZmxvd1Rlc3QuamF2YTozMTc5KVxuc3VuLnJlZmxlY3QuTmF0aXZlTWV0aG9kQWNjZXNzb3JJbXBsLmludm9rZTAoTmF0aXZlIE1ldGhvZClcbnN1bi5yZWZsZWN0Lk5hdGl2ZU1ldGhvZEFjY2Vzc29ySW1wbC5pbnZva2UoTmF0aXZlTWV0aG9kQWNjZXNzb3JJbXBsLmphdmE6NjIpXG5zdW4ucmVmbGVjdC5EZWxlZ2F0aW5nTWV0aG9kQWNjZXNzb3JJbXBsLmludm9rZShEZWxlZ2F0aW5nTWV0aG9kQWNjZXNzb3JJbXBsLmphdmE6NDMpXG5qYXZhLmxhbmcucmVmbGVjdC5NZXRob2QuaW52b2tlKE1ldGhvZC5qYXZhOjQ5OClcbmlvLnRlbXBvcmFsLmludGVybmFsLnN5bmMuUE9KT1dvcmtmbG93SW1wbGVtZW50YXRpb25GYWN0b3J5JFBPSk9Xb3JrZmxvd0ltcGxlbWVudGF0aW9uJFJvb3RXb3JrZmxvd0ludm9jYXRpb25JbnRlcmNlcHRvci5leGVjdXRlKFBPSk9Xb3JrZmxvd0ltcGxlbWVudGF0aW9uRmFjdG9yeS5qYXZhOjI4MylcbmlvLnRlbXBvcmFsLmNvbW1vbi5pbnRlcmNlcHRvcnMuQmFzZVdvcmtmbG93SW52b2tlci5leGVjdXRlKEJhc2VXb3JrZmxvd0ludm9rZXIuamF2YTozOSlcbmlvLnRlbXBvcmFsLmludGVybmFsLnN5bmMuUE9KT1dvcmtmbG93SW1wbGVtZW50YXRpb25GYWN0b3J5JFBPSk9Xb3JrZmxvd0ltcGxlbWVudGF0aW9uLmV4ZWN1dGUoUE9KT1dvcmtmbG93SW1wbGVtZW50YXRpb25GYWN0b3J5LmphdmE6MjQ3KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwuc3luYy5Xb3JrZmxvd0V4ZWN1dGVSdW5uYWJsZS5ydW4oV29ya2Zsb3dFeGVjdXRlUnVubmFibGUuamF2YTo1MilcbmlvLnRlbXBvcmFsLmludGVybmFsLnN5bmMuU3luY1dvcmtmbG93LmxhbWJkYSRzdGFydCQwKFN5bmNXb3JrZmxvdy5qYXZhOjExNylcbmlvLnRlbXBvcmFsLmludGVybmFsLnN5bmMuQ2FuY2VsbGF0aW9uU2NvcGVJbXBsLnJ1bihDYW5jZWxsYXRpb25TY29wZUltcGwuamF2YToxMDQpXG5pby50ZW1wb3JhbC5pbnRlcm5hbC5zeW5jLldvcmtmbG93VGhyZWFkSW1wbCRSdW5uYWJsZVdyYXBwZXIucnVuKFdvcmtmbG93VGhyZWFkSW1wbC5qYXZhOjEwNilcbmphdmEudXRpbC5jb25jdXJyZW50LkV4ZWN1dG9ycyRSdW5uYWJsZUFkYXB0ZXIuY2FsbChFeGVjdXRvcnMuamF2YTo1MTEpXG5qYXZhLnV0aWwuY29uY3VycmVudC5GdXR1cmVUYXNrLnJ1bihGdXR1cmVUYXNrLmphdmE6MjY2KVxuamF2YS51dGlsLmNvbmN1cnJlbnQuVGhyZWFkUG9vbEV4ZWN1dG9yLnJ1bldvcmtlcihUaHJlYWRQb29sRXhlY3V0b3IuamF2YToxMTQ5KVxuamF2YS51dGlsLmNvbmN1cnJlbnQuVGhyZWFkUG9vbEV4ZWN1dG9yJFdvcmtlci5ydW4oVGhyZWFkUG9vbEV4ZWN1dG9yLmphdmE6NjI0KVxuamF2YS5sYW5nLlRocmVhZC5ydW4oVGhyZWFkLmphdmE6NzQ4KVxuIiwic3VwcHJlc3NlZEV4Y2VwdGlvbnMiOltdLCJjbGFzcyI6ImphdmEubGFuZy5VbnN1cHBvcnRlZE9wZXJhdGlvbkV4Y2VwdGlvbiJ9" - }] + "failure": { + "message": "simulated failure", + "source": "JavaSDK", + "stackTrace": "io.temporal.workflow.WorkflowTest$AngryChild.execute(WorkflowTest.java:3237)\nsun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\nsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\nsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\njava.lang.reflect.Method.invoke(Method.java:498)\nio.temporal.internal.sync.POJOWorkflowImplementationFactory$POJOWorkflowImplementation$RootWorkflowInvocationInterceptor.execute(POJOWorkflowImplementationFactory.java:284)\nio.temporal.common.interceptors.BaseWorkflowInvoker.execute(BaseWorkflowInvoker.java:39)\nio.temporal.internal.sync.POJOWorkflowImplementationFactory$POJOWorkflowImplementation.execute(POJOWorkflowImplementationFactory.java:248)\nio.temporal.internal.sync.WorkflowExecuteRunnable.run(WorkflowExecuteRunnable.java:52)\nio.temporal.internal.sync.SyncWorkflow.lambda$start$0(SyncWorkflow.java:118)\nio.temporal.internal.sync.CancellationScopeImpl.run(CancellationScopeImpl.java:104)\nio.temporal.internal.sync.WorkflowThreadImpl$RunnableWrapper.run(WorkflowThreadImpl.java:106)\njava.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)\njava.util.concurrent.FutureTask.run(FutureTask.java:266)\njava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\njava.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\njava.lang.Thread.run(Thread.java:748)\n", + "applicationFailureInfo": { + "type": "java.lang.UnsupportedOperationException" + } }, "workflowExecution": { - "workflowId": "0d162baa-a415-34b0-8f85-6fe5638f32cc", - "runId": "57204659-d3be-4839-b1fa-0205233e8169" + "workflowId": "8ac3a14e-a0fb-3c8a-881c-96f18d8ed768", + "runId": "a69c48c1-4006-4e51-8351-5670dd32cbb4" }, "workflowType": { "name": "ITestChild" }, "initiatedEventId": "5", - "startedEventId": "6" + "startedEventId": "6", + "retryStatus": "MaximumAttemptsReached" } }, { "eventId": "11", - "timestamp": "1590453588355270900", + "timestamp": "1591983168978773000", "eventType": "DecisionTaskScheduled", "version": "-24", - "taskId": "1061776", + "taskId": "5253154", "decisionTaskScheduledEventAttributes": { "taskList": { - "name": "unknown-mac:834bd77a-77e9-4209-9029-95eba1365b1c" + "name": "unknown-mac:bb59cb91-cbd7-482d-bc86-9b662f0d0127" }, "startToCloseTimeoutSeconds": 2 } }, { "eventId": "12", - "timestamp": "1590453588362213600", + "timestamp": "1591983168985659400", "eventType": "DecisionTaskStarted", "version": "-24", - "taskId": "1061780", + "taskId": "5253158", "decisionTaskStartedEventAttributes": { "scheduledEventId": "11", - "identity": "834bd77a-77e9-4209-9029-95eba1365b1c", - "requestId": "61621446-35b8-4842-ab67-a0b5e910ba14" + "identity": "bb59cb91-cbd7-482d-bc86-9b662f0d0127", + "requestId": "dae377be-e3c5-4220-85ad-854439ad2c3b" } }, { "eventId": "13", - "timestamp": "1590453588372046900", + "timestamp": "1591983168995428100", "eventType": "DecisionTaskCompleted", "version": "-24", - "taskId": "1061783", + "taskId": "5253161", "decisionTaskCompletedEventAttributes": { "scheduledEventId": "11", "startedEventId": "12", @@ -210,20 +210,34 @@ } }, { "eventId": "14", - "timestamp": "1590453588372081100", + "timestamp": "1591983168995456500", "eventType": "WorkflowExecutionFailed", "version": "-24", - "taskId": "1061784", + "taskId": "5253162", "workflowExecutionFailedEventAttributes": { - "reason": "io.temporal.workflow.ChildWorkflowFailureException", - "details": { - "payloads": [{ - "metadata": { - "encoding": "anNvbg==" + "failure": { + "source": "JavaSDK", + "stackTrace": "java.lang.Thread.getStackTrace(Thread.java:1559)\nio.temporal.internal.sync.ChildWorkflowStubImpl.execute(ChildWorkflowStubImpl.java:84)\nio.temporal.internal.sync.ChildWorkflowInvocationHandler.invoke(ChildWorkflowInvocationHandler.java:74)\ncom.sun.proxy.$Proxy49.execute(Unknown Source)\nio.temporal.workflow.WorkflowTest$TestChildWorkflowRetryWorkflow.execute(WorkflowTest.java:3200)\nsun.reflect.GeneratedMethodAccessor10.invoke(Unknown Source)\nsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\njava.lang.reflect.Method.invoke(Method.java:498)\nio.temporal.internal.sync.POJOWorkflowImplementationFactory$POJOWorkflowImplementation$RootWorkflowInvocationInterceptor.execute(POJOWorkflowImplementationFactory.java:284)\nio.temporal.common.interceptors.BaseWorkflowInvoker.execute(BaseWorkflowInvoker.java:39)\nio.temporal.internal.sync.POJOWorkflowImplementationFactory$POJOWorkflowImplementation.execute(POJOWorkflowImplementationFactory.java:248)\nio.temporal.internal.sync.WorkflowExecuteRunnable.run(WorkflowExecuteRunnable.java:52)\nio.temporal.internal.sync.SyncWorkflow.lambda$start$0(SyncWorkflow.java:118)\nio.temporal.internal.sync.CancellationScopeImpl.run(CancellationScopeImpl.java:104)\nio.temporal.internal.sync.WorkflowThreadImpl$RunnableWrapper.run(WorkflowThreadImpl.java:106)\njava.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)\njava.util.concurrent.FutureTask.run(FutureTask.java:266)\njava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\njava.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\njava.lang.Thread.run(Thread.java:748)\n", + "cause": { + "message": "simulated failure", + "source": "JavaSDK", + "stackTrace": "io.temporal.workflow.WorkflowTest$AngryChild.execute(WorkflowTest.java:3237)\nsun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\nsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\nsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\njava.lang.reflect.Method.invoke(Method.java:498)\nio.temporal.internal.sync.POJOWorkflowImplementationFactory$POJOWorkflowImplementation$RootWorkflowInvocationInterceptor.execute(POJOWorkflowImplementationFactory.java:284)\nio.temporal.common.interceptors.BaseWorkflowInvoker.execute(BaseWorkflowInvoker.java:39)\nio.temporal.internal.sync.POJOWorkflowImplementationFactory$POJOWorkflowImplementation.execute(POJOWorkflowImplementationFactory.java:248)\nio.temporal.internal.sync.WorkflowExecuteRunnable.run(WorkflowExecuteRunnable.java:52)\nio.temporal.internal.sync.SyncWorkflow.lambda$start$0(SyncWorkflow.java:118)\nio.temporal.internal.sync.CancellationScopeImpl.run(CancellationScopeImpl.java:104)\nio.temporal.internal.sync.WorkflowThreadImpl$RunnableWrapper.run(WorkflowThreadImpl.java:106)\njava.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)\njava.util.concurrent.FutureTask.run(FutureTask.java:266)\njava.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)\njava.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)\njava.lang.Thread.run(Thread.java:748)\n", + "applicationFailureInfo": { + "type": "java.lang.UnsupportedOperationException" + } + }, + "childWorkflowExecutionFailureInfo": { + "workflowExecution": { + "workflowId": "8ac3a14e-a0fb-3c8a-881c-96f18d8ed768", + "runId": "a69c48c1-4006-4e51-8351-5670dd32cbb4" }, - "data": "eyJ3b3JrZmxvd0V4ZWN1dGlvbiI6eyJ3b3JrZmxvd0lkXyI6IjBkMTYyYmFhLWE0MTUtMzRiMC04Zjg1LTZmZTU2MzhmMzJjYyIsInJ1bklkXyI6IjU3MjA0NjU5LWQzYmUtNDgzOS1iMWZhLTAyMDUyMzNlODE2OSIsIm1lbW9pemVkSXNJbml0aWFsaXplZCI6LTEsInVua25vd25GaWVsZHMiOnsiZmllbGRzIjp7fSwiZmllbGRzRGVzY2VuZGluZyI6e319LCJtZW1vaXplZFNpemUiOi0xLCJtZW1vaXplZEhhc2hDb2RlIjowfSwid29ya2Zsb3dUeXBlIjp7Im5hbWVfIjoiSVRlc3RDaGlsZCIsIm1lbW9pemVkSXNJbml0aWFsaXplZCI6LTEsInVua25vd25GaWVsZHMiOnsiZmllbGRzIjp7fSwiZmllbGRzRGVzY2VuZGluZyI6e319LCJtZW1vaXplZFNpemUiOi0xLCJtZW1vaXplZEhhc2hDb2RlIjowfSwiZXZlbnRJZCI6MTAsImRldGFpbE1lc3NhZ2UiOiJzaW11bGF0ZWQgZmFpbHVyZSBXb3JrZmxvd1R5cGVcdTAwM2RcIklUZXN0Q2hpbGRcIiwgSURcdTAwM2RcIjBkMTYyYmFhLWE0MTUtMzRiMC04Zjg1LTZmZTU2MzhmMzJjY1wiLCBSdW5JZFx1MDAzZFwiNTcyMDQ2NTktZDNiZS00ODM5LWIxZmEtMDIwNTIzM2U4MTY5LCBFdmVudElkXHUwMDNkMTAiLCJjYXVzZSI6eyJkZXRhaWxNZXNzYWdlIjoic2ltdWxhdGVkIGZhaWx1cmUiLCJzdGFja1RyYWNlIjoiaW8udGVtcG9yYWwud29ya2Zsb3cuV29ya2Zsb3dUZXN0JEFuZ3J5Q2hpbGQuZXhlY3V0ZShXb3JrZmxvd1Rlc3QuamF2YTozMTc5KVxuc3VuLnJlZmxlY3QuTmF0aXZlTWV0aG9kQWNjZXNzb3JJbXBsLmludm9rZTAoTmF0aXZlIE1ldGhvZDowKVxuc3VuLnJlZmxlY3QuTmF0aXZlTWV0aG9kQWNjZXNzb3JJbXBsLmludm9rZShOYXRpdmVNZXRob2RBY2Nlc3NvckltcGwuamF2YTo2MilcbnN1bi5yZWZsZWN0LkRlbGVnYXRpbmdNZXRob2RBY2Nlc3NvckltcGwuaW52b2tlKERlbGVnYXRpbmdNZXRob2RBY2Nlc3NvckltcGwuamF2YTo0MylcbmphdmEubGFuZy5yZWZsZWN0Lk1ldGhvZC5pbnZva2UoTWV0aG9kLmphdmE6NDk4KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwuc3luYy5QT0pPV29ya2Zsb3dJbXBsZW1lbnRhdGlvbkZhY3RvcnkkUE9KT1dvcmtmbG93SW1wbGVtZW50YXRpb24kUm9vdFdvcmtmbG93SW52b2NhdGlvbkludGVyY2VwdG9yLmV4ZWN1dGUoUE9KT1dvcmtmbG93SW1wbGVtZW50YXRpb25GYWN0b3J5LmphdmE6MjgzKVxuaW8udGVtcG9yYWwuY29tbW9uLmludGVyY2VwdG9ycy5CYXNlV29ya2Zsb3dJbnZva2VyLmV4ZWN1dGUoQmFzZVdvcmtmbG93SW52b2tlci5qYXZhOjM5KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwuc3luYy5QT0pPV29ya2Zsb3dJbXBsZW1lbnRhdGlvbkZhY3RvcnkkUE9KT1dvcmtmbG93SW1wbGVtZW50YXRpb24uZXhlY3V0ZShQT0pPV29ya2Zsb3dJbXBsZW1lbnRhdGlvbkZhY3RvcnkuamF2YToyNDcpXG5pby50ZW1wb3JhbC5pbnRlcm5hbC5zeW5jLldvcmtmbG93RXhlY3V0ZVJ1bm5hYmxlLnJ1bihXb3JrZmxvd0V4ZWN1dGVSdW5uYWJsZS5qYXZhOjUyKVxuaW8udGVtcG9yYWwuaW50ZXJuYWwuc3luYy5TeW5jV29ya2Zsb3cubGFtYmRhJHN0YXJ0JDAoU3luY1dvcmtmbG93LmphdmE6MTE3KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwuc3luYy5DYW5jZWxsYXRpb25TY29wZUltcGwucnVuKENhbmNlbGxhdGlvblNjb3BlSW1wbC5qYXZhOjEwNClcbmlvLnRlbXBvcmFsLmludGVybmFsLnN5bmMuV29ya2Zsb3dUaHJlYWRJbXBsJFJ1bm5hYmxlV3JhcHBlci5ydW4oV29ya2Zsb3dUaHJlYWRJbXBsLmphdmE6MTA2KVxuamF2YS51dGlsLmNvbmN1cnJlbnQuRXhlY3V0b3JzJFJ1bm5hYmxlQWRhcHRlci5jYWxsKEV4ZWN1dG9ycy5qYXZhOjUxMSlcbmphdmEudXRpbC5jb25jdXJyZW50LkZ1dHVyZVRhc2sucnVuKEZ1dHVyZVRhc2suamF2YToyNjYpXG5qYXZhLnV0aWwuY29uY3VycmVudC5UaHJlYWRQb29sRXhlY3V0b3IucnVuV29ya2VyKFRocmVhZFBvb2xFeGVjdXRvci5qYXZhOjExNDkpXG5qYXZhLnV0aWwuY29uY3VycmVudC5UaHJlYWRQb29sRXhlY3V0b3IkV29ya2VyLnJ1bihUaHJlYWRQb29sRXhlY3V0b3IuamF2YTo2MjQpXG5qYXZhLmxhbmcuVGhyZWFkLnJ1bihUaHJlYWQuamF2YTo3NDgpXG4iLCJzdXBwcmVzc2VkRXhjZXB0aW9ucyI6W10sImNsYXNzIjoiamF2YS5sYW5nLlVuc3VwcG9ydGVkT3BlcmF0aW9uRXhjZXB0aW9uIn0sInN0YWNrVHJhY2UiOiJqYXZhLmxhbmcuVGhyZWFkLmdldFN0YWNrVHJhY2UoVGhyZWFkLmphdmE6MTU1OSlcbmlvLnRlbXBvcmFsLmludGVybmFsLnN5bmMuQ2hpbGRXb3JrZmxvd1N0dWJJbXBsLmV4ZWN1dGUoQ2hpbGRXb3JrZmxvd1N0dWJJbXBsLmphdmE6ODQpXG5pby50ZW1wb3JhbC5pbnRlcm5hbC5zeW5jLkNoaWxkV29ya2Zsb3dJbnZvY2F0aW9uSGFuZGxlci5pbnZva2UoQ2hpbGRXb3JrZmxvd0ludm9jYXRpb25IYW5kbGVyLmphdmE6NzQpXG5jb20uc3VuLnByb3h5LiRQcm94eTQ2LmV4ZWN1dGUoVW5rbm93biBTb3VyY2UpXG5pby50ZW1wb3JhbC53b3JrZmxvdy5Xb3JrZmxvd1Rlc3QkVGVzdENoaWxkV29ya2Zsb3dSZXRyeVdvcmtmbG93LmV4ZWN1dGUoV29ya2Zsb3dUZXN0LmphdmE6MzE0MilcbnN1bi5yZWZsZWN0LkdlbmVyYXRlZE1ldGhvZEFjY2Vzc29yMTQuaW52b2tlKFVua25vd24gU291cmNlKVxuc3VuLnJlZmxlY3QuRGVsZWdhdGluZ01ldGhvZEFjY2Vzc29ySW1wbC5pbnZva2UoRGVsZWdhdGluZ01ldGhvZEFjY2Vzc29ySW1wbC5qYXZhOjQzKVxuamF2YS5sYW5nLnJlZmxlY3QuTWV0aG9kLmludm9rZShNZXRob2QuamF2YTo0OTgpXG5pby50ZW1wb3JhbC5pbnRlcm5hbC5zeW5jLlBPSk9Xb3JrZmxvd0ltcGxlbWVudGF0aW9uRmFjdG9yeSRQT0pPV29ya2Zsb3dJbXBsZW1lbnRhdGlvbiRSb290V29ya2Zsb3dJbnZvY2F0aW9uSW50ZXJjZXB0b3IuZXhlY3V0ZShQT0pPV29ya2Zsb3dJbXBsZW1lbnRhdGlvbkZhY3RvcnkuamF2YToyODMpXG5pby50ZW1wb3JhbC5jb21tb24uaW50ZXJjZXB0b3JzLkJhc2VXb3JrZmxvd0ludm9rZXIuZXhlY3V0ZShCYXNlV29ya2Zsb3dJbnZva2VyLmphdmE6MzkpXG5pby50ZW1wb3JhbC5pbnRlcm5hbC5zeW5jLlBPSk9Xb3JrZmxvd0ltcGxlbWVudGF0aW9uRmFjdG9yeSRQT0pPV29ya2Zsb3dJbXBsZW1lbnRhdGlvbi5leGVjdXRlKFBPSk9Xb3JrZmxvd0ltcGxlbWVudGF0aW9uRmFjdG9yeS5qYXZhOjI0NylcbmlvLnRlbXBvcmFsLmludGVybmFsLnN5bmMuV29ya2Zsb3dFeGVjdXRlUnVubmFibGUucnVuKFdvcmtmbG93RXhlY3V0ZVJ1bm5hYmxlLmphdmE6NTIpXG5pby50ZW1wb3JhbC5pbnRlcm5hbC5zeW5jLlN5bmNXb3JrZmxvdy5sYW1iZGEkc3RhcnQkMChTeW5jV29ya2Zsb3cuamF2YToxMTcpXG5pby50ZW1wb3JhbC5pbnRlcm5hbC5zeW5jLkNhbmNlbGxhdGlvblNjb3BlSW1wbC5ydW4oQ2FuY2VsbGF0aW9uU2NvcGVJbXBsLmphdmE6MTA0KVxuaW8udGVtcG9yYWwuaW50ZXJuYWwuc3luYy5Xb3JrZmxvd1RocmVhZEltcGwkUnVubmFibGVXcmFwcGVyLnJ1bihXb3JrZmxvd1RocmVhZEltcGwuamF2YToxMDYpXG5qYXZhLnV0aWwuY29uY3VycmVudC5FeGVjdXRvcnMkUnVubmFibGVBZGFwdGVyLmNhbGwoRXhlY3V0b3JzLmphdmE6NTExKVxuamF2YS51dGlsLmNvbmN1cnJlbnQuRnV0dXJlVGFzay5ydW4oRnV0dXJlVGFzay5qYXZhOjI2NilcbmphdmEudXRpbC5jb25jdXJyZW50LlRocmVhZFBvb2xFeGVjdXRvci5ydW5Xb3JrZXIoVGhyZWFkUG9vbEV4ZWN1dG9yLmphdmE6MTE0OSlcbmphdmEudXRpbC5jb25jdXJyZW50LlRocmVhZFBvb2xFeGVjdXRvciRXb3JrZXIucnVuKFRocmVhZFBvb2xFeGVjdXRvci5qYXZhOjYyNClcbmphdmEubGFuZy5UaHJlYWQucnVuKFRocmVhZC5qYXZhOjc0OClcbiIsInN1cHByZXNzZWRFeGNlcHRpb25zIjpbXSwiY2xhc3MiOiJpby50ZW1wb3JhbC53b3JrZmxvdy5DaGlsZFdvcmtmbG93RmFpbHVyZUV4Y2VwdGlvbiJ9" - }] + "workflowType": { + "name": "ITestChild" + }, + "retryStatus": "MaximumAttemptsReached" + } }, + "retryStatus": "RetryPolicyNotSet", "decisionTaskCompletedEventId": "13" } }]