Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 4 additions & 2 deletions client/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ compileTestJava {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
options.fork = true
options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac"
def javacName = org.gradle.internal.os.OperatingSystem.current().isWindows() ? 'javac.exe' : 'javac'
options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/${javacName}"
}

task downloadProtoFiles {
Expand Down Expand Up @@ -110,7 +111,8 @@ sourceSets {
}

tasks.withType(Test) {
executable = new File("${PATH_TO_TEST_JAVA_RUNTIME}", 'bin/java')
def javaName = org.gradle.internal.os.OperatingSystem.current().isWindows() ? 'java.exe' : 'java'
executable = new File("${PATH_TO_TEST_JAVA_RUNTIME}", "bin/${javaName}")
}

test {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public final class DurableTaskGrpcWorker implements AutoCloseable {
private final DataConverter dataConverter;
private final Duration maximumTimerInterval;
private final DurableTaskGrpcWorkerVersioningOptions versioningOptions;
private final ExceptionPropertiesProvider exceptionPropertiesProvider;

private final TaskHubSidecarServiceBlockingStub sidecarClient;

Expand Down Expand Up @@ -70,6 +71,7 @@ public final class DurableTaskGrpcWorker implements AutoCloseable {
this.dataConverter = builder.dataConverter != null ? builder.dataConverter : new JacksonDataConverter();
this.maximumTimerInterval = builder.maximumTimerInterval != null ? builder.maximumTimerInterval : DEFAULT_MAXIMUM_TIMER_INTERVAL;
this.versioningOptions = builder.versioningOptions;
this.exceptionPropertiesProvider = builder.exceptionPropertiesProvider;
}

/**
Expand Down Expand Up @@ -123,7 +125,8 @@ public void startAndBlock() {
this.dataConverter,
this.maximumTimerInterval,
logger,
this.versioningOptions);
this.versioningOptions,
this.exceptionPropertiesProvider);
TaskActivityExecutor taskActivityExecutor = new TaskActivityExecutor(
this.activityFactories,
this.dataConverter,
Expand Down Expand Up @@ -351,11 +354,9 @@ public void startAndBlock() {
activityRequest.getTaskId());
} catch (Throwable e) {
activityError = e;
failureDetails = TaskFailureDetails.newBuilder()
.setErrorType(e.getClass().getName())
.setErrorMessage(e.getMessage())
.setStackTrace(StringValue.of(FailureDetails.getFullStackTrace(e)))
.build();
Exception ex = e instanceof Exception ? (Exception) e : new RuntimeException(e);
failureDetails = FailureDetails.fromException(
ex, this.exceptionPropertiesProvider).toProto();
} finally {
activityScope.close();
TracingHelper.endSpan(activitySpan, activityError);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public final class DurableTaskGrpcWorkerBuilder {
DataConverter dataConverter;
Duration maximumTimerInterval;
DurableTaskGrpcWorkerVersioningOptions versioningOptions;
ExceptionPropertiesProvider exceptionPropertiesProvider;

/**
* Adds an orchestration factory to be used by the constructed {@link DurableTaskGrpcWorker}.
Expand Down Expand Up @@ -125,6 +126,21 @@ public DurableTaskGrpcWorkerBuilder useVersioning(DurableTaskGrpcWorkerVersionin
return this;
}

/**
* Sets the {@link ExceptionPropertiesProvider} to use for extracting custom properties from exceptions.
* <p>
* When set, the provider is invoked whenever an activity or orchestration fails with an exception. The returned
* properties are included in the {@link FailureDetails} and can be retrieved via
* {@link FailureDetails#getProperties()}.
*
* @param provider the exception properties provider
* @return this builder object
*/
public DurableTaskGrpcWorkerBuilder exceptionPropertiesProvider(ExceptionPropertiesProvider provider) {
this.exceptionPropertiesProvider = provider;
return this;
}

/**
* Initializes a new {@link DurableTaskGrpcWorker} object with the settings specified in the current builder object.
* @return a new {@link DurableTaskGrpcWorker} object
Expand Down
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);
}
194 changes: 189 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.ListValue;
import com.google.protobuf.NullValue;
import com.google.protobuf.StringValue;
import com.google.protobuf.Struct;
import com.google.protobuf.Value;
import com.microsoft.durabletask.implementation.protobuf.OrchestratorService.TaskFailureDetails;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;

/**
* Class that represents the details of a task failure.
Expand All @@ -20,29 +25,62 @@ 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,
fromExceptionRecursive(exception.getCause(), null, 1),
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) {
return fromExceptionRecursive(exception, provider, 0);
}

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()));
}

/**
Expand Down Expand Up @@ -86,6 +124,31 @@ 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() {
if (this.properties == null) {
return null;
}
return Collections.unmodifiableMap(this.properties);
}

/**
* Returns {@code true} if the task failure was provided by the specified exception type, otherwise {@code false}.
* <p>
Expand All @@ -112,6 +175,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();

Expand All @@ -124,10 +192,126 @@ 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();
}

private static final int MAX_INNER_FAILURE_DEPTH = 10;

@Nullable
private static FailureDetails fromExceptionRecursive(
@Nullable Throwable exception,
@Nullable ExceptionPropertiesProvider provider,
int depth) {
if (exception == null || depth > MAX_INNER_FAILURE_DEPTH) {
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,
fromExceptionRecursive(exception.getCause(), provider, depth + 1),
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();
case LIST_VALUE:
List<Object> list = new ArrayList<>();
for (Value item : value.getListValue().getValuesList()) {
list.add(convertProtoValue(item));
}
return list;
case STRUCT_VALUE:
Map<String, Object> map = new HashMap<>();
for (Map.Entry<String, Value> entry : value.getStructValue().getFieldsMap().entrySet()) {
map.put(entry.getKey(), convertProtoValue(entry.getValue()));
}
return map;
default:
return value.toString();
}
}

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;
}

@SuppressWarnings("unchecked")
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 if (obj instanceof List) {
ListValue.Builder listBuilder = ListValue.newBuilder();
for (Object item : (List<?>) obj) {
listBuilder.addValues(convertToProtoValue(item));
}
return Value.newBuilder().setListValue(listBuilder).build();
} else if (obj instanceof Map) {
Struct.Builder structBuilder = Struct.newBuilder();
for (Map.Entry<String, Object> entry : ((Map<String, Object>) obj).entrySet()) {
structBuilder.putFields(entry.getKey(), convertToProtoValue(entry.getValue()));
}
return Value.newBuilder().setStructValue(structBuilder).build();
} else {
return Value.newBuilder().setStringValue(obj.toString()).build();
}
}
}
}
Loading
Loading