Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -228,11 +228,8 @@ public void startAndBlock() {
activityRequest.getInput().getValue(),
activityRequest.getTaskId());
} catch (Throwable e) {
failureDetails = TaskFailureDetails.newBuilder()
.setErrorType(e.getClass().getName())
.setErrorMessage(e.getMessage())
.setStackTrace(StringValue.of(FailureDetails.getFullStackTrace(e)))
.build();
failureDetails = new FailureDetails(
e instanceof Exception ? (Exception) e : new RuntimeException(e)).toProto();
}

ActivityResponse.Builder responseBuilder = ActivityResponse.newBuilder()
Expand Down
141 changes: 136 additions & 5 deletions client/src/main/java/com/microsoft/durabletask/FailureDetails.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
// Licensed under the MIT License.
package com.microsoft.durabletask;

import com.google.protobuf.NullValue;
import com.google.protobuf.StringValue;
import com.google.protobuf.Value;
import com.microsoft.durabletask.implementation.protobuf.OrchestratorService.TaskFailureDetails;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
* Class that represents the details of a task failure.
Expand All @@ -20,29 +25,50 @@ public final class FailureDetails {
private final String errorMessage;
private final String stackTrace;
private final boolean isNonRetriable;
private final FailureDetails innerFailure;
private final Map<String, Object> properties;

FailureDetails(
String errorType,
@Nullable String errorMessage,
@Nullable String errorDetails,
boolean isNonRetriable) {
this(errorType, errorMessage, errorDetails, isNonRetriable, null, null);
}

FailureDetails(
String errorType,
@Nullable String errorMessage,
@Nullable String errorDetails,
boolean isNonRetriable,
@Nullable FailureDetails innerFailure,
@Nullable Map<String, Object> properties) {
this.errorType = errorType;
this.stackTrace = errorDetails;

// Error message can be null for things like NullPointerException but the gRPC contract doesn't allow null
this.errorMessage = errorMessage != null ? errorMessage : "";
this.isNonRetriable = isNonRetriable;
this.innerFailure = innerFailure;
this.properties = properties != null ? Collections.unmodifiableMap(new HashMap<>(properties)) : null;
}

FailureDetails(Exception exception) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is FailureDetails(Exception exception) used anywhere?

this(exception.getClass().getName(), exception.getMessage(), getFullStackTrace(exception), false);
this(exception.getClass().getName(),
exception.getMessage(),
getFullStackTrace(exception),
false,
exception.getCause() != null ? fromExceptionRecursive(exception.getCause()) : null,
null);
}

FailureDetails(TaskFailureDetails proto) {
this(proto.getErrorType(),
proto.getErrorMessage(),
proto.getStackTrace().getValue(),
proto.getIsNonRetriable());
proto.getIsNonRetriable(),
proto.hasInnerFailure() ? new FailureDetails(proto.getInnerFailure()) : null,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a depth limit for this recursive call too?

convertProtoProperties(proto.getPropertiesMap()));
}

/**
Expand Down Expand Up @@ -86,6 +112,28 @@ public boolean isNonRetriable() {
return this.isNonRetriable;
}

/**
* Gets the inner failure that caused this failure, or {@code null} if there is no inner cause.
*
* @return the inner {@code FailureDetails} or {@code null}
*/
@Nullable
public FailureDetails getInnerFailure() {
return this.innerFailure;
}

/**
* Gets additional properties associated with the exception, or {@code null} if no properties are available.
* <p>
* The returned map is unmodifiable.
*
* @return an unmodifiable map of property names to values, or {@code null}
*/
@Nullable
public Map<String, Object> getProperties() {
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
return this.properties;
}

/**
* Returns {@code true} if the task failure was provided by the specified exception type, otherwise {@code false}.
* <p>
Expand All @@ -112,6 +160,11 @@ public boolean isCausedBy(Class<? extends Exception> exceptionClass) {
}
}
Comment on lines 165 to 176
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new innerFailure field is not tested with the isCausedBy method. It's unclear whether isCausedBy should only check the current failure or should traverse the innerFailure chain (similar to how one might need to check both an exception and its causes). Consider adding tests to clarify and document this behavior, or update the isCausedBy method to traverse the chain if that's the intended behavior.

Copilot uses AI. Check for mistakes.

@Override
public String toString() {
return this.errorType + ": " + this.errorMessage;
}

static String getFullStackTrace(Throwable e) {
StackTraceElement[] elements = e.getStackTrace();

Expand All @@ -124,10 +177,88 @@ static String getFullStackTrace(Throwable e) {
}

TaskFailureDetails toProto() {
return TaskFailureDetails.newBuilder()
TaskFailureDetails.Builder builder = TaskFailureDetails.newBuilder()
.setErrorType(this.getErrorType())
.setErrorMessage(this.getErrorMessage())
.setStackTrace(StringValue.of(this.getStackTrace() != null ? this.getStackTrace() : ""))
.build();
.setIsNonRetriable(this.isNonRetriable);

if (this.innerFailure != null) {
builder.setInnerFailure(this.innerFailure.toProto());
}

if (this.properties != null) {
builder.putAllProperties(convertToProtoProperties(this.properties));
}

return builder.build();
}

@Nullable
private static FailureDetails fromExceptionRecursive(@Nullable Throwable exception) {
if (exception == null) {
return null;
}
return new FailureDetails(
exception.getClass().getName(),
exception.getMessage(),
getFullStackTrace(exception),
false,
exception.getCause() != null ? fromExceptionRecursive(exception.getCause()) : null,
null);
}

@Nullable
private static Map<String, Object> convertProtoProperties(Map<String, Value> protoProperties) {
if (protoProperties == null || protoProperties.isEmpty()) {
return null;
}

Map<String, Object> result = new HashMap<>();
for (Map.Entry<String, Value> entry : protoProperties.entrySet()) {
result.put(entry.getKey(), convertProtoValue(entry.getValue()));
}
return result;
}

@Nullable
private static Object convertProtoValue(Value value) {
if (value == null) {
return null;
}
switch (value.getKindCase()) {
case NULL_VALUE:
return null;
case NUMBER_VALUE:
return value.getNumberValue();
case STRING_VALUE:
return value.getStringValue();
case BOOL_VALUE:
return value.getBoolValue();
default:
return value.toString();
}
Comment thread
nytian marked this conversation as resolved.
}

private static Map<String, Value> convertToProtoProperties(Map<String, Object> properties) {
Map<String, Value> result = new HashMap<>();
for (Map.Entry<String, Object> entry : properties.entrySet()) {
result.put(entry.getKey(), convertToProtoValue(entry.getValue()));
}
return result;
}

private static Value convertToProtoValue(@Nullable Object obj) {
if (obj == null) {
return Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build();
} else if (obj instanceof Number) {
return Value.newBuilder().setNumberValue(((Number) obj).doubleValue()).build();
} else if (obj instanceof Boolean) {
return Value.newBuilder().setBoolValue((Boolean) obj).build();
} else if (obj instanceof String) {
return Value.newBuilder().setStringValue((String) obj).build();
} else {
return Value.newBuilder().setStringValue(obj.toString()).build();
}
}
}
}
Loading
Loading