-
Notifications
You must be signed in to change notification settings - Fork 16
Include Exception Properties at FailureDetails #263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
| package com.microsoft.durabletask; | ||
|
|
||
| import javax.annotation.Nullable; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Provider interface for extracting custom properties from exceptions. | ||
| * <p> | ||
| * Implementations of this interface can be registered with a {@link DurableTaskGrpcWorkerBuilder} to include | ||
| * custom exception properties in {@link FailureDetails} when activities or orchestrations fail. | ||
| * These properties are then available via {@link FailureDetails#getProperties()}. | ||
| * <p> | ||
| * Example usage: | ||
| * <pre>{@code | ||
| * DurableTaskGrpcWorker worker = new DurableTaskGrpcWorkerBuilder() | ||
| * .exceptionPropertiesProvider(exception -> { | ||
| * if (exception instanceof MyCustomException) { | ||
| * MyCustomException custom = (MyCustomException) exception; | ||
| * Map<String, Object> props = new HashMap<>(); | ||
| * props.put("errorCode", custom.getErrorCode()); | ||
| * props.put("retryable", custom.isRetryable()); | ||
| * return props; | ||
| * } | ||
| * return null; | ||
| * }) | ||
| * .addOrchestration(...) | ||
| * .build(); | ||
| * }</pre> | ||
| */ | ||
| @FunctionalInterface | ||
| public interface ExceptionPropertiesProvider { | ||
|
|
||
| /** | ||
| * Extracts custom properties from the given exception. | ||
| * <p> | ||
| * Return {@code null} or an empty map if no custom properties should be included for this exception. | ||
| * | ||
| * @param exception the exception to extract properties from | ||
| * @return a map of property names to values, or {@code null} | ||
| */ | ||
| @Nullable | ||
| Map<String, Object> getExceptionProperties(Exception exception); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
|
@@ -20,29 +25,76 @@ 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) { | ||
| 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, | ||
| null); | ||
| } | ||
|
|
||
| /** | ||
| * Creates a {@code FailureDetails} from an exception, optionally using the provided | ||
| * {@link ExceptionPropertiesProvider} to extract custom properties. | ||
| * | ||
| * @param exception the exception that caused the failure | ||
| * @param provider the provider for extracting custom properties, or {@code null} | ||
| * @return a new {@code FailureDetails} instance | ||
| */ | ||
| static FailureDetails fromException(Exception exception, @Nullable ExceptionPropertiesProvider provider) { | ||
| Map<String, Object> properties = null; | ||
| if (provider != null) { | ||
| try { | ||
| properties = provider.getExceptionProperties(exception); | ||
| } catch (Exception ignored) { | ||
| // Don't let provider errors mask the original failure | ||
| } | ||
| } | ||
| return new FailureDetails( | ||
| exception.getClass().getName(), | ||
| exception.getMessage(), | ||
| getFullStackTrace(exception), | ||
| false, | ||
| exception.getCause() != null ? fromExceptionRecursive(exception.getCause(), provider) : null, | ||
| properties); | ||
| } | ||
|
|
||
| FailureDetails(TaskFailureDetails proto) { | ||
| this(proto.getErrorType(), | ||
| proto.getErrorMessage(), | ||
| proto.getStackTrace().getValue(), | ||
| proto.getIsNonRetriable()); | ||
| proto.getIsNonRetriable(), | ||
| proto.hasInnerFailure() ? new FailureDetails(proto.getInnerFailure()) : null, | ||
| convertProtoProperties(proto.getPropertiesMap())); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -86,6 +138,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() { | ||
|
|
||
| return this.properties; | ||
| } | ||
|
|
||
| /** | ||
| * Returns {@code true} if the task failure was provided by the specified exception type, otherwise {@code false}. | ||
| * <p> | ||
|
|
@@ -112,6 +186,11 @@ public boolean isCausedBy(Class<? extends Exception> exceptionClass) { | |
| } | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return this.errorType + ": " + this.errorMessage; | ||
| } | ||
|
|
||
| static String getFullStackTrace(Throwable e) { | ||
| StackTraceElement[] elements = e.getStackTrace(); | ||
|
|
||
|
|
@@ -124,10 +203,98 @@ 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, | ||
| @Nullable ExceptionPropertiesProvider provider) { | ||
| if (exception == null) { | ||
| return null; | ||
| } | ||
| Map<String, Object> properties = null; | ||
| if (provider != null && exception instanceof Exception) { | ||
| try { | ||
| properties = provider.getExceptionProperties((Exception) exception); | ||
| } catch (Exception ignored) { | ||
| // Don't let provider errors mask the original failure | ||
| } | ||
| } | ||
| return new FailureDetails( | ||
| exception.getClass().getName(), | ||
| exception.getMessage(), | ||
| getFullStackTrace(exception), | ||
| false, | ||
| exception.getCause() != null ? fromExceptionRecursive(exception.getCause(), provider) : null, | ||
| properties); | ||
| } | ||
|
|
||
| @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 on lines
+265
to
+276
|
||
| } | ||
|
|
||
| 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(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.