Skip to content

Commit 17f1ec2

Browse files
committed
udpate
1 parent f4c3777 commit 17f1ec2

File tree

7 files changed

+234
-11
lines changed

7 files changed

+234
-11
lines changed

client/build.gradle

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ compileTestJava {
5757
sourceCompatibility = JavaVersion.VERSION_11
5858
targetCompatibility = JavaVersion.VERSION_11
5959
options.fork = true
60-
options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/javac"
60+
def javacName = org.gradle.internal.os.OperatingSystem.current().isWindows() ? 'javac.exe' : 'javac'
61+
options.forkOptions.executable = "${PATH_TO_TEST_JAVA_RUNTIME}/bin/${javacName}"
6162
}
6263

6364
task downloadProtoFiles {
@@ -90,7 +91,9 @@ protobuf {
9091
}
9192
generateProtoTasks {
9293
all()*.plugins { grpc {} }
93-
all()*.dependsOn downloadProtoFiles
94+
if (project.gradle.startParameter.taskNames.any { it.contains('downloadProtoFiles') }) {
95+
all()*.dependsOn downloadProtoFiles
96+
}
9497
}
9598
}
9699

@@ -107,7 +110,8 @@ sourceSets {
107110
}
108111

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

113117
test {

client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorker.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public final class DurableTaskGrpcWorker implements AutoCloseable {
3434
private final DataConverter dataConverter;
3535
private final Duration maximumTimerInterval;
3636
private final DurableTaskGrpcWorkerVersioningOptions versioningOptions;
37+
private final ExceptionPropertiesProvider exceptionPropertiesProvider;
3738

3839
private final TaskHubSidecarServiceBlockingStub sidecarClient;
3940

@@ -65,6 +66,7 @@ public final class DurableTaskGrpcWorker implements AutoCloseable {
6566
this.dataConverter = builder.dataConverter != null ? builder.dataConverter : new JacksonDataConverter();
6667
this.maximumTimerInterval = builder.maximumTimerInterval != null ? builder.maximumTimerInterval : DEFAULT_MAXIMUM_TIMER_INTERVAL;
6768
this.versioningOptions = builder.versioningOptions;
69+
this.exceptionPropertiesProvider = builder.exceptionPropertiesProvider;
6870
}
6971

7072
/**
@@ -118,7 +120,8 @@ public void startAndBlock() {
118120
this.dataConverter,
119121
this.maximumTimerInterval,
120122
logger,
121-
this.versioningOptions);
123+
this.versioningOptions,
124+
this.exceptionPropertiesProvider);
122125
TaskActivityExecutor taskActivityExecutor = new TaskActivityExecutor(
123126
this.activityFactories,
124127
this.dataConverter,
@@ -228,8 +231,9 @@ public void startAndBlock() {
228231
activityRequest.getInput().getValue(),
229232
activityRequest.getTaskId());
230233
} catch (Throwable e) {
231-
failureDetails = new FailureDetails(
232-
e instanceof Exception ? (Exception) e : new RuntimeException(e)).toProto();
234+
Exception ex = e instanceof Exception ? (Exception) e : new RuntimeException(e);
235+
failureDetails = FailureDetails.fromException(
236+
ex, this.exceptionPropertiesProvider).toProto();
233237
}
234238

235239
ActivityResponse.Builder responseBuilder = ActivityResponse.newBuilder()

client/src/main/java/com/microsoft/durabletask/DurableTaskGrpcWorkerBuilder.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public final class DurableTaskGrpcWorkerBuilder {
1818
DataConverter dataConverter;
1919
Duration maximumTimerInterval;
2020
DurableTaskGrpcWorkerVersioningOptions versioningOptions;
21+
ExceptionPropertiesProvider exceptionPropertiesProvider;
2122

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

129+
/**
130+
* Sets the {@link ExceptionPropertiesProvider} to use for extracting custom properties from exceptions.
131+
* <p>
132+
* When set, the provider is invoked whenever an activity or orchestration fails with an exception. The returned
133+
* properties are included in the {@link FailureDetails} and can be retrieved via
134+
* {@link FailureDetails#getProperties()}.
135+
*
136+
* @param provider the exception properties provider
137+
* @return this builder object
138+
*/
139+
public DurableTaskGrpcWorkerBuilder exceptionPropertiesProvider(ExceptionPropertiesProvider provider) {
140+
this.exceptionPropertiesProvider = provider;
141+
return this;
142+
}
143+
128144
/**
129145
* Initializes a new {@link DurableTaskGrpcWorker} object with the settings specified in the current builder object.
130146
* @return a new {@link DurableTaskGrpcWorker} object
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.microsoft.durabletask;
4+
5+
import javax.annotation.Nullable;
6+
import java.util.Map;
7+
8+
/**
9+
* Provider interface for extracting custom properties from exceptions.
10+
* <p>
11+
* Implementations of this interface can be registered with a {@link DurableTaskGrpcWorkerBuilder} to include
12+
* custom exception properties in {@link FailureDetails} when activities or orchestrations fail.
13+
* These properties are then available via {@link FailureDetails#getProperties()}.
14+
* <p>
15+
* Example usage:
16+
* <pre>{@code
17+
* DurableTaskGrpcWorker worker = new DurableTaskGrpcWorkerBuilder()
18+
* .exceptionPropertiesProvider(exception -> {
19+
* if (exception instanceof MyCustomException) {
20+
* MyCustomException custom = (MyCustomException) exception;
21+
* Map<String, Object> props = new HashMap<>();
22+
* props.put("errorCode", custom.getErrorCode());
23+
* props.put("retryable", custom.isRetryable());
24+
* return props;
25+
* }
26+
* return null;
27+
* })
28+
* .addOrchestration(...)
29+
* .build();
30+
* }</pre>
31+
*/
32+
@FunctionalInterface
33+
public interface ExceptionPropertiesProvider {
34+
35+
/**
36+
* Extracts custom properties from the given exception.
37+
* <p>
38+
* Return {@code null} or an empty map if no custom properties should be included for this exception.
39+
*
40+
* @param exception the exception to extract properties from
41+
* @return a map of property names to values, or {@code null}
42+
*/
43+
@Nullable
44+
Map<String, Object> getExceptionProperties(Exception exception);
45+
}

client/src/main/java/com/microsoft/durabletask/FailureDetails.java

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,36 @@ public final class FailureDetails {
5858
exception.getMessage(),
5959
getFullStackTrace(exception),
6060
false,
61-
exception.getCause() != null ? fromExceptionRecursive(exception.getCause()) : null,
61+
exception.getCause() != null ? fromExceptionRecursive(exception.getCause(), null) : null,
6262
null);
6363
}
6464

65+
/**
66+
* Creates a {@code FailureDetails} from an exception, optionally using the provided
67+
* {@link ExceptionPropertiesProvider} to extract custom properties.
68+
*
69+
* @param exception the exception that caused the failure
70+
* @param provider the provider for extracting custom properties, or {@code null}
71+
* @return a new {@code FailureDetails} instance
72+
*/
73+
static FailureDetails fromException(Exception exception, @Nullable ExceptionPropertiesProvider provider) {
74+
Map<String, Object> properties = null;
75+
if (provider != null) {
76+
try {
77+
properties = provider.getExceptionProperties(exception);
78+
} catch (Exception ignored) {
79+
// Don't let provider errors mask the original failure
80+
}
81+
}
82+
return new FailureDetails(
83+
exception.getClass().getName(),
84+
exception.getMessage(),
85+
getFullStackTrace(exception),
86+
false,
87+
exception.getCause() != null ? fromExceptionRecursive(exception.getCause(), provider) : null,
88+
properties);
89+
}
90+
6591
FailureDetails(TaskFailureDetails proto) {
6692
this(proto.getErrorType(),
6793
proto.getErrorMessage(),
@@ -195,17 +221,27 @@ TaskFailureDetails toProto() {
195221
}
196222

197223
@Nullable
198-
private static FailureDetails fromExceptionRecursive(@Nullable Throwable exception) {
224+
private static FailureDetails fromExceptionRecursive(
225+
@Nullable Throwable exception,
226+
@Nullable ExceptionPropertiesProvider provider) {
199227
if (exception == null) {
200228
return null;
201229
}
230+
Map<String, Object> properties = null;
231+
if (provider != null && exception instanceof Exception) {
232+
try {
233+
properties = provider.getExceptionProperties((Exception) exception);
234+
} catch (Exception ignored) {
235+
// Don't let provider errors mask the original failure
236+
}
237+
}
202238
return new FailureDetails(
203239
exception.getClass().getName(),
204240
exception.getMessage(),
205241
getFullStackTrace(exception),
206242
false,
207-
exception.getCause() != null ? fromExceptionRecursive(exception.getCause()) : null,
208-
null);
243+
exception.getCause() != null ? fromExceptionRecursive(exception.getCause(), provider) : null,
244+
properties);
209245
}
210246

211247
@Nullable

client/src/main/java/com/microsoft/durabletask/TaskOrchestrationExecutor.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,30 @@ final class TaskOrchestrationExecutor {
3131
private final Logger logger;
3232
private final Duration maximumTimerInterval;
3333
private final DurableTaskGrpcWorkerVersioningOptions versioningOptions;
34+
private final ExceptionPropertiesProvider exceptionPropertiesProvider;
3435

3536
public TaskOrchestrationExecutor(
3637
HashMap<String, TaskOrchestrationFactory> orchestrationFactories,
3738
DataConverter dataConverter,
3839
Duration maximumTimerInterval,
3940
Logger logger,
4041
DurableTaskGrpcWorkerVersioningOptions versioningOptions) {
42+
this(orchestrationFactories, dataConverter, maximumTimerInterval, logger, versioningOptions, null);
43+
}
44+
45+
public TaskOrchestrationExecutor(
46+
HashMap<String, TaskOrchestrationFactory> orchestrationFactories,
47+
DataConverter dataConverter,
48+
Duration maximumTimerInterval,
49+
Logger logger,
50+
DurableTaskGrpcWorkerVersioningOptions versioningOptions,
51+
ExceptionPropertiesProvider exceptionPropertiesProvider) {
4152
this.orchestrationFactories = orchestrationFactories;
4253
this.dataConverter = dataConverter;
4354
this.maximumTimerInterval = maximumTimerInterval;
4455
this.logger = logger;
4556
this.versioningOptions = versioningOptions;
57+
this.exceptionPropertiesProvider = exceptionPropertiesProvider;
4658
}
4759

4860
public TaskOrchestratorResult execute(List<HistoryEvent> pastEvents, List<HistoryEvent> newEvents) {
@@ -68,7 +80,7 @@ public TaskOrchestratorResult execute(List<HistoryEvent> pastEvents, List<Histor
6880
// The orchestrator threw an unhandled exception - fail it
6981
// TODO: What's the right way to log this?
7082
logger.warning("The orchestrator failed with an unhandled exception: " + e.toString());
71-
context.fail(new FailureDetails(e));
83+
context.fail(FailureDetails.fromException(e, this.exceptionPropertiesProvider));
7284
}
7385

7486
if ((context.continuedAsNew && !context.isComplete) || (completed && context.pendingActions.isEmpty() && !context.waitingForEvents())) {

client/src/test/java/com/microsoft/durabletask/FailureDetailsTest.java

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,112 @@ void constructFromProto_withInnerFailureAndProperties() {
385385
assertEquals(false, details.getInnerFailure().getProperties().get("retryable"));
386386
}
387387

388+
@Test
389+
void fromException_withProvider_extractsProperties() {
390+
ExceptionPropertiesProvider provider = exception -> {
391+
if (exception instanceof IllegalArgumentException) {
392+
Map<String, Object> props = new HashMap<>();
393+
props.put("paramName", exception.getMessage());
394+
props.put("severity", 3);
395+
props.put("isCritical", true);
396+
return props;
397+
}
398+
return null;
399+
};
400+
401+
IllegalArgumentException ex = new IllegalArgumentException("userId");
402+
403+
FailureDetails details = FailureDetails.fromException(ex, provider);
404+
405+
assertEquals("java.lang.IllegalArgumentException", details.getErrorType());
406+
assertEquals("userId", details.getErrorMessage());
407+
assertNotNull(details.getProperties());
408+
assertEquals(3, details.getProperties().size());
409+
assertEquals("userId", details.getProperties().get("paramName"));
410+
assertEquals(3, details.getProperties().get("severity"));
411+
assertEquals(true, details.getProperties().get("isCritical"));
412+
}
413+
414+
@Test
415+
void fromException_withProvider_propertiesOnInnerCauseToo() {
416+
ExceptionPropertiesProvider provider = exception -> {
417+
Map<String, Object> props = new HashMap<>();
418+
props.put("exceptionType", exception.getClass().getSimpleName());
419+
return props;
420+
};
421+
422+
IOException inner = new IOException("disk full");
423+
RuntimeException outer = new RuntimeException("failed", inner);
424+
425+
FailureDetails details = FailureDetails.fromException(outer, provider);
426+
427+
assertNotNull(details.getProperties());
428+
assertEquals("RuntimeException", details.getProperties().get("exceptionType"));
429+
430+
assertNotNull(details.getInnerFailure());
431+
assertNotNull(details.getInnerFailure().getProperties());
432+
assertEquals("IOException", details.getInnerFailure().getProperties().get("exceptionType"));
433+
}
434+
435+
@Test
436+
void fromException_withProvider_returnsNull_noProperties() {
437+
ExceptionPropertiesProvider provider = exception -> null;
438+
439+
RuntimeException ex = new RuntimeException("test");
440+
441+
FailureDetails details = FailureDetails.fromException(ex, provider);
442+
443+
assertNull(details.getProperties());
444+
}
445+
446+
@Test
447+
void fromException_withNullProvider_noProperties() {
448+
RuntimeException ex = new RuntimeException("test");
449+
450+
FailureDetails details = FailureDetails.fromException(ex, null);
451+
452+
assertEquals("java.lang.RuntimeException", details.getErrorType());
453+
assertEquals("test", details.getErrorMessage());
454+
assertNull(details.getProperties());
455+
}
456+
457+
@Test
458+
void fromException_providerThrows_gracefullyIgnored() {
459+
ExceptionPropertiesProvider provider = exception -> {
460+
throw new RuntimeException("provider error");
461+
};
462+
463+
IllegalStateException ex = new IllegalStateException("original error");
464+
465+
FailureDetails details = FailureDetails.fromException(ex, provider);
466+
467+
assertEquals("java.lang.IllegalStateException", details.getErrorType());
468+
assertEquals("original error", details.getErrorMessage());
469+
assertNull(details.getProperties());
470+
}
471+
472+
@Test
473+
void fromException_withProvider_roundTripsViaProto() {
474+
ExceptionPropertiesProvider provider = exception -> {
475+
Map<String, Object> props = new HashMap<>();
476+
props.put("errorCode", "VALIDATION_FAILED");
477+
props.put("retryCount", 3);
478+
props.put("isCritical", true);
479+
return props;
480+
};
481+
482+
IllegalArgumentException ex = new IllegalArgumentException("bad input");
483+
FailureDetails details = FailureDetails.fromException(ex, provider);
484+
485+
TaskFailureDetails proto = details.toProto();
486+
FailureDetails roundTripped = new FailureDetails(proto);
487+
488+
assertNotNull(roundTripped.getProperties());
489+
assertEquals("VALIDATION_FAILED", roundTripped.getProperties().get("errorCode"));
490+
assertEquals(3.0, roundTripped.getProperties().get("retryCount"));
491+
assertEquals(true, roundTripped.getProperties().get("isCritical"));
492+
}
493+
388494
@Test
389495
void constructFromProto_withProperties_containsNullKey() {
390496
// Properties map with a null-valued entry should be preserved

0 commit comments

Comments
 (0)