Skip to content

Commit d0b62cf

Browse files
committed
address comments
1 parent 6b21ad1 commit d0b62cf

6 files changed

Lines changed: 83 additions & 193 deletions

File tree

‎azurefunctions/build.gradle‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ dependencies {
3737
api project(':client')
3838
implementation group: 'com.microsoft.azure.functions', name: 'azure-functions-java-library', version: '3.2.3'
3939
implementation "com.google.protobuf:protobuf-java:${protocVersion}"
40+
implementation "com.google.protobuf:protobuf-java-util:${protocVersion}"
4041
compileOnly "com.microsoft.azure.functions:azure-functions-java-spi:1.1.0"
4142

4243
// Test dependencies

‎azurefunctions/src/main/java/com/microsoft/durabletask/azurefunctions/internal/middleware/ActivityMiddleware.java‎

Lines changed: 24 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66

77
package com.microsoft.durabletask.azurefunctions.internal.middleware;
88

9+
import com.google.protobuf.InvalidProtocolBufferException;
10+
import com.google.protobuf.util.JsonFormat;
911
import com.microsoft.azure.functions.internal.spi.middleware.Middleware;
1012
import com.microsoft.azure.functions.internal.spi.middleware.MiddlewareChain;
1113
import com.microsoft.azure.functions.internal.spi.middleware.MiddlewareContext;
1214
import com.microsoft.durabletask.ExceptionPropertiesProvider;
15+
import com.microsoft.durabletask.FailureDetails;
1316

1417
import java.lang.reflect.InvocationTargetException;
1518
import java.util.Iterator;
@@ -36,7 +39,8 @@
3639
public class ActivityMiddleware implements Middleware {
3740

3841
private static final String ACTIVITY_TRIGGER = "DurableActivityTrigger";
39-
private static final int MAX_INNER_FAILURE_DEPTH = 10;
42+
private static final JsonFormat.Printer FAILURE_DETAILS_JSON_PRINTER =
43+
JsonFormat.printer().omittingInsignificantWhitespace();
4044
private static final Logger LOGGER = Logger.getLogger(ActivityMiddleware.class.getName());
4145

4246
private static final Object PROVIDER_LOCK = new Object();
@@ -68,14 +72,21 @@ public void invoke(MiddlewareContext context, MiddlewareChain chain) throws Exce
6872
throw e;
6973
}
7074

71-
Throwable userException = unwrap(e);
72-
String failureDetailsJson = buildFailureDetailsJson(userException, provider);
73-
if (failureDetailsJson == null) {
75+
FailureDetails failureDetails = FailureDetails.fromException(unwrap(e), provider);
76+
if (!hasCustomProperties(failureDetails)) {
7477
// No custom properties for this failure chain - preserve the original behavior.
7578
throw e;
7679
}
7780

78-
throw new StructuredActivityFailure(failureDetailsJson);
81+
try {
82+
throw new StructuredActivityFailure(
83+
FAILURE_DETAILS_JSON_PRINTER.print(failureDetails.toProto()));
84+
} catch (InvalidProtocolBufferException serializationException) {
85+
LOGGER.log(Level.WARNING,
86+
"Failed to serialize structured failure details; rethrowing the original exception.",
87+
serializationException);
88+
throw e;
89+
}
7990
}
8091
}
8192

@@ -176,183 +187,16 @@ private static Throwable unwrap(Throwable e) {
176187
return current;
177188
}
178189

179-
/**
180-
* Invokes the provider defensively, returning {@code null} if the failure is not an
181-
* {@link Exception} or the provider itself throws, so a misbehaving provider never masks the
182-
* original failure.
183-
*/
184-
private static Map<String, Object> safeGetProperties(
185-
ExceptionPropertiesProvider provider,
186-
Throwable exception) {
187-
if (!(exception instanceof Exception)) {
188-
return null;
189-
}
190-
try {
191-
return provider.getExceptionProperties((Exception) exception);
192-
} catch (Exception providerException) {
193-
// Don't let a misbehaving provider mask the original failure.
194-
LOGGER.log(Level.WARNING,
195-
"ExceptionPropertiesProvider threw while extracting properties; ignoring provider output.",
196-
providerException);
197-
return null;
198-
}
199-
}
200-
201-
/**
202-
* Builds the single-line JSON payload that mirrors the protobuf {@code TaskFailureDetails} shape
203-
* consumed by the Durable Task host extension.
204-
*/
205-
private static String buildFailureDetailsJson(
206-
Throwable exception,
207-
ExceptionPropertiesProvider provider) {
208-
StringBuilder sb = new StringBuilder(256);
209-
return appendFailure(sb, exception, provider, 0) ? sb.toString() : null;
210-
}
211-
212-
/**
213-
* Recursively appends one failure level (error type/message/stack trace, any custom properties,
214-
* and the cause as a nested {@code innerFailure}) to the JSON buffer.
215-
*/
216-
private static boolean appendFailure(
217-
StringBuilder sb,
218-
Throwable exception,
219-
ExceptionPropertiesProvider provider,
220-
int depth) {
221-
Map<String, Object> properties = safeGetProperties(provider, exception);
222-
boolean hasCustomProperties = properties != null && !properties.isEmpty();
223-
224-
sb.append('{');
225-
sb.append("\"errorType\":");
226-
appendString(sb, exception.getClass().getName());
227-
sb.append(",\"errorMessage\":");
228-
appendString(sb, exception.getMessage() != null ? exception.getMessage() : "");
229-
sb.append(",\"stackTrace\":");
230-
appendString(sb, getFullStackTrace(exception));
231-
sb.append(",\"isNonRetriable\":false");
232-
233-
if (properties != null && !properties.isEmpty()) {
234-
sb.append(",\"properties\":");
235-
appendValue(sb, properties);
236-
}
237-
238-
Throwable cause = exception.getCause();
239-
if (cause != null && cause != exception && depth + 1 < MAX_INNER_FAILURE_DEPTH) {
240-
sb.append(",\"innerFailure\":");
241-
hasCustomProperties |= appendFailure(sb, cause, provider, depth + 1);
242-
}
243-
244-
sb.append('}');
245-
return hasCustomProperties;
246-
}
247-
248-
/**
249-
* Serializes a single property value as JSON, handling strings, booleans, numbers (non-finite
250-
* doubles fall back to strings), maps, iterables, and arrays; anything else is written as its
251-
* {@code toString()}.
252-
*/
253-
@SuppressWarnings("unchecked")
254-
private static void appendValue(StringBuilder sb, Object value) {
255-
if (value == null) {
256-
sb.append("null");
257-
} else if (value instanceof String) {
258-
appendString(sb, (String) value);
259-
} else if (value instanceof Boolean) {
260-
sb.append(((Boolean) value) ? "true" : "false");
261-
} else if (value instanceof Double || value instanceof Float) {
262-
double d = ((Number) value).doubleValue();
263-
if (Double.isNaN(d) || Double.isInfinite(d)) {
264-
appendString(sb, value.toString());
265-
} else {
266-
sb.append(value.toString());
267-
}
268-
} else if (value instanceof Number) {
269-
sb.append(value.toString());
270-
} else if (value instanceof Map) {
271-
sb.append('{');
272-
boolean first = true;
273-
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
274-
if (!first) {
275-
sb.append(',');
276-
}
277-
first = false;
278-
appendString(sb, String.valueOf(entry.getKey()));
279-
sb.append(':');
280-
appendValue(sb, entry.getValue());
281-
}
282-
sb.append('}');
283-
} else if (value instanceof Iterable) {
284-
sb.append('[');
285-
boolean first = true;
286-
for (Object item : (Iterable<Object>) value) {
287-
if (!first) {
288-
sb.append(',');
289-
}
290-
first = false;
291-
appendValue(sb, item);
292-
}
293-
sb.append(']');
294-
} else if (value instanceof Object[]) {
295-
sb.append('[');
296-
Object[] array = (Object[]) value;
297-
for (int i = 0; i < array.length; i++) {
298-
if (i > 0) {
299-
sb.append(',');
300-
}
301-
appendValue(sb, array[i]);
302-
}
303-
sb.append(']');
304-
} else {
305-
appendString(sb, value.toString());
306-
}
307-
}
308-
309-
/** Appends {@code value} as a JSON string literal, escaping quotes, backslashes, and control characters. */
310-
private static void appendString(StringBuilder sb, String value) {
311-
sb.append('"');
312-
for (int i = 0; i < value.length(); i++) {
313-
char c = value.charAt(i);
314-
switch (c) {
315-
case '"':
316-
sb.append("\\\"");
317-
break;
318-
case '\\':
319-
sb.append("\\\\");
320-
break;
321-
case '\n':
322-
sb.append("\\n");
323-
break;
324-
case '\r':
325-
sb.append("\\r");
326-
break;
327-
case '\t':
328-
sb.append("\\t");
329-
break;
330-
case '\b':
331-
sb.append("\\b");
332-
break;
333-
case '\f':
334-
sb.append("\\f");
335-
break;
336-
default:
337-
if (c < 0x20) {
338-
sb.append(String.format("\\u%04x", (int) c));
339-
} else {
340-
sb.append(c);
341-
}
342-
break;
190+
private static boolean hasCustomProperties(FailureDetails failureDetails) {
191+
for (FailureDetails current = failureDetails;
192+
current != null;
193+
current = current.getInnerFailure()) {
194+
Map<String, Object> properties = current.getProperties();
195+
if (properties != null && !properties.isEmpty()) {
196+
return true;
343197
}
344198
}
345-
sb.append('"');
346-
}
347-
348-
/** Formats the throwable's stack trace as newline-separated {@code \tat ...} frames. */
349-
private static String getFullStackTrace(Throwable e) {
350-
StackTraceElement[] elements = e.getStackTrace();
351-
StringBuilder sb = new StringBuilder(elements.length * 64);
352-
for (StackTraceElement element : elements) {
353-
sb.append("\tat ").append(element.toString()).append(System.lineSeparator());
354-
}
355-
return sb.toString();
199+
return false;
356200
}
357201

358202
/**

‎azurefunctions/src/test/java/com/microsoft/durabletask/azurefunctions/internal/middleware/ActivityMiddlewareTest.java‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
// Licensed under the MIT License.
33
package com.microsoft.durabletask.azurefunctions.internal.middleware;
44

5+
import com.google.protobuf.InvalidProtocolBufferException;
6+
import com.google.protobuf.util.JsonFormat;
57
import com.microsoft.azure.functions.internal.spi.middleware.MiddlewareChain;
68
import com.microsoft.azure.functions.internal.spi.middleware.MiddlewareContext;
79
import com.microsoft.durabletask.ExceptionPropertiesProvider;
10+
import com.microsoft.durabletask.implementation.protobuf.OrchestratorService.TaskFailureDetails;
811
import org.junit.jupiter.api.AfterEach;
912
import org.junit.jupiter.api.BeforeEach;
1013
import org.junit.jupiter.api.DisplayName;
@@ -28,6 +31,7 @@
2831
import static org.junit.jupiter.api.Assertions.assertFalse;
2932
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
3033
import static org.junit.jupiter.api.Assertions.assertNotNull;
34+
import static org.junit.jupiter.api.Assertions.assertNotSame;
3135
import static org.junit.jupiter.api.Assertions.assertNull;
3236
import static org.junit.jupiter.api.Assertions.assertSame;
3337
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -112,11 +116,12 @@ void resetAfter() {
112116
@Test
113117
@DisplayName("Reshapes a failing activity into structured TaskFailureDetails JSON when the "
114118
+ "provider yields properties")
115-
void reshapesFailureWhenProviderYieldsProperties() {
119+
void reshapesFailureWhenProviderYieldsProperties() throws InvalidProtocolBufferException {
116120
setProviderSupplier(() -> exception -> {
117121
Map<String, Object> properties = new LinkedHashMap<>();
118122
properties.put("code", "E123");
119123
properties.put("count", 7);
124+
properties.put("attempts", new int[] {1, 2});
120125
return properties;
121126
});
122127

@@ -127,7 +132,7 @@ void reshapesFailureWhenProviderYieldsProperties() {
127132
() -> middleware.invoke(activityContext(), throwingChain(original)));
128133

129134
// The original exception is replaced by a structured-failure carrier whose message is JSON.
130-
assertNotSameInstance(original, thrown);
135+
assertNotSame(original, thrown);
131136
String message = thrown.getMessage();
132137
assertNotNull(message);
133138
assertTrue(message.startsWith("{"), "message should be a JSON object, was: " + message);
@@ -136,6 +141,10 @@ void reshapesFailureWhenProviderYieldsProperties() {
136141
assertTrue(message.contains("\"errorMessage\":\"boom\""), message);
137142
assertTrue(message.contains("\"code\":\"E123\""), message);
138143
assertTrue(message.contains("\"count\":7"), message);
144+
145+
TaskFailureDetails failureDetails = parseFailureDetails(message);
146+
assertFalse(failureDetails.getIsNonRetriable());
147+
assertEquals(2, failureDetails.getPropertiesMap().get("attempts").getListValue().getValuesCount());
139148
}
140149

141150
@Test
@@ -261,7 +270,7 @@ void reshapesFailureWhenOnlyInnerCauseYieldsProperties() {
261270
Exception thrown = assertThrows(Exception.class,
262271
() -> middleware.invoke(activityContext(), throwingChain(outer)));
263272

264-
assertFalse(outer == thrown, "the inner provider properties should produce structured details");
273+
assertNotSame(outer, thrown, "the inner provider properties should produce structured details");
265274
String message = thrown.getMessage();
266275
assertNotNull(message);
267276
assertTrue(message.contains("\"innerFailure\":{"), message);
@@ -360,8 +369,9 @@ private URLClassLoader newClassLoaderExposingProvider(ClassLoader parent) throws
360369
return new URLClassLoader(new URL[] {rootUrl}, parent);
361370
}
362371

363-
private static void assertNotSameInstance(Object unexpected, Object actual) {
364-
assertFalse(unexpected == actual,
365-
"expected a different instance than the original exception");
372+
private static TaskFailureDetails parseFailureDetails(String json) throws InvalidProtocolBufferException {
373+
TaskFailureDetails.Builder builder = TaskFailureDetails.newBuilder();
374+
JsonFormat.parser().merge(json, builder);
375+
return builder.build();
366376
}
367377
}

‎client/build.gradle‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def exeSuffix = isWindows ? ".exe" : ""
3939
dependencies {
4040

4141
// https://github.com/grpc/grpc-java#download
42+
api "com.google.protobuf:protobuf-java:${protocVersion}"
4243
implementation "io.grpc:grpc-protobuf:${grpcVersion}"
4344
implementation "io.grpc:grpc-stub:${grpcVersion}"
4445
runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}"

0 commit comments

Comments
 (0)