Skip to content
Merged
10 changes: 7 additions & 3 deletions client/build.gradle
Comment thread
nytian marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,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 @@ -90,7 +91,9 @@ protobuf {
}
generateProtoTasks {
all()*.plugins { grpc {} }
all()*.dependsOn downloadProtoFiles
if (project.gradle.startParameter.taskNames.any { it.contains('downloadProtoFiles') }) {
all()*.dependsOn downloadProtoFiles
}
}
}

Expand All @@ -107,7 +110,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 @@ -34,6 +34,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 @@ -65,6 +66,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 @@ -118,7 +120,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 @@ -228,11 +231,9 @@ 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();
Exception ex = e instanceof Exception ? (Exception) e : new RuntimeException(e);
Comment thread
nytian marked this conversation as resolved.
Outdated
failureDetails = FailureDetails.fromException(
ex, this.exceptionPropertiesProvider).toProto();
}

ActivityResponse.Builder responseBuilder = ActivityResponse.newBuilder()
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);
}
177 changes: 172 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,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) {
Comment thread
nytian marked this conversation as resolved.
Outdated
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) {
Comment thread
nytian marked this conversation as resolved.
Outdated
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,
Comment thread
bachuv marked this conversation as resolved.
convertProtoProperties(proto.getPropertiesMap()));
}

/**
Expand Down Expand Up @@ -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() {
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
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 +186,11 @@ public boolean isCausedBy(Class<? extends Exception> exceptionClass) {
}
}
Comment thread
nytian marked this conversation as resolved.

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

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

Expand All @@ -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(
Comment thread
nytian marked this conversation as resolved.
@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
Comment thread
nytian marked this conversation as resolved.
Outdated
}
}
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 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