|
6 | 6 |
|
7 | 7 | package com.microsoft.durabletask.azurefunctions.internal.middleware; |
8 | 8 |
|
| 9 | +import com.google.protobuf.InvalidProtocolBufferException; |
| 10 | +import com.google.protobuf.util.JsonFormat; |
9 | 11 | import com.microsoft.azure.functions.internal.spi.middleware.Middleware; |
10 | 12 | import com.microsoft.azure.functions.internal.spi.middleware.MiddlewareChain; |
11 | 13 | import com.microsoft.azure.functions.internal.spi.middleware.MiddlewareContext; |
12 | 14 | import com.microsoft.durabletask.ExceptionPropertiesProvider; |
| 15 | +import com.microsoft.durabletask.FailureDetails; |
13 | 16 |
|
14 | 17 | import java.lang.reflect.InvocationTargetException; |
15 | 18 | import java.util.Iterator; |
|
36 | 39 | public class ActivityMiddleware implements Middleware { |
37 | 40 |
|
38 | 41 | 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(); |
40 | 44 | private static final Logger LOGGER = Logger.getLogger(ActivityMiddleware.class.getName()); |
41 | 45 |
|
42 | 46 | private static final Object PROVIDER_LOCK = new Object(); |
@@ -68,14 +72,21 @@ public void invoke(MiddlewareContext context, MiddlewareChain chain) throws Exce |
68 | 72 | throw e; |
69 | 73 | } |
70 | 74 |
|
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)) { |
74 | 77 | // No custom properties for this failure chain - preserve the original behavior. |
75 | 78 | throw e; |
76 | 79 | } |
77 | 80 |
|
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 | + } |
79 | 90 | } |
80 | 91 | } |
81 | 92 |
|
@@ -176,183 +187,16 @@ private static Throwable unwrap(Throwable e) { |
176 | 187 | return current; |
177 | 188 | } |
178 | 189 |
|
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; |
343 | 197 | } |
344 | 198 | } |
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; |
356 | 200 | } |
357 | 201 |
|
358 | 202 | /** |
|
0 commit comments