Skip to content

Commit 04290b7

Browse files
authored
Include Exception Types at FailureDetails (#1237)
* initial commit * udpate test * udpate by commit * udpate * add comments * add special case when taskcontext is null * update comment * update by comments * make two internal property public and also update typo * make properties object nullable and add test case
1 parent a2ffc8b commit 04290b7

16 files changed

+422
-29
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// ----------------------------------------------------------------------------------
2+
// Copyright Microsoft Corporation
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ----------------------------------------------------------------------------------
13+
#nullable enable
14+
namespace DurableTask.Core
15+
{
16+
using System;
17+
using System.Collections.Generic;
18+
using DurableTask.Core.Exceptions;
19+
20+
/// <summary>
21+
/// Extension methods for <see cref="IExceptionPropertiesProvider"/>.
22+
/// </summary>
23+
public static class ExceptionPropertiesProviderExtensions
24+
{
25+
/// <summary>
26+
/// Extracts properties of the exception specified at provider.
27+
/// </summary>
28+
public static IDictionary<string, object?>? ExtractProperties(this IExceptionPropertiesProvider? provider, Exception exception)
29+
{
30+
if (exception is OrchestrationException orchestrationException &&
31+
orchestrationException.FailureDetails?.Properties != null)
32+
{
33+
return orchestrationException.FailureDetails.Properties;
34+
}
35+
36+
if (provider == null)
37+
{
38+
return null;
39+
}
40+
41+
return provider.GetExceptionProperties(exception);
42+
}
43+
}
44+
}
45+
46+

src/DurableTask.Core/FailureDetails.cs

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,29 @@ public class FailureDetails : IEquatable<FailureDetails>
3838
/// <param name="stackTrace">The exception stack trace.</param>
3939
/// <param name="innerFailure">The inner cause of the failure.</param>
4040
/// <param name="isNonRetriable">Whether the failure is non-retriable.</param>
41+
/// <param name="properties">Additional properties associated with the failure.</param>
4142
[JsonConstructor]
42-
public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable)
43+
public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable, IDictionary<string, object?>? properties = null)
4344
{
4445
this.ErrorType = errorType;
4546
this.ErrorMessage = errorMessage;
4647
this.StackTrace = stackTrace;
4748
this.InnerFailure = innerFailure;
4849
this.IsNonRetriable = isNonRetriable;
50+
this.Properties = properties;
51+
}
52+
53+
/// <summary>
54+
/// Initializes a new instance of the <see cref="FailureDetails"/> class.
55+
/// </summary>
56+
/// <param name="errorType">The name of the error, which is expected to the the namespace-qualified name of the exception type.</param>
57+
/// <param name="errorMessage">The message associated with the error, which is expected to be the exception's <see cref="Exception.Message"/> property.</param>
58+
/// <param name="stackTrace">The exception stack trace.</param>
59+
/// <param name="innerFailure">The inner cause of the failure.</param>
60+
/// <param name="isNonRetriable">Whether the failure is non-retriable.</param>
61+
public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable)
62+
: this(errorType, errorMessage, stackTrace, innerFailure, isNonRetriable, properties:null)
63+
{
4964
}
5065

5166
/// <summary>
@@ -54,7 +69,7 @@ public FailureDetails(string errorType, string errorMessage, string? stackTrace,
5469
/// <param name="e">The exception used to generate the failure details.</param>
5570
/// <param name="innerFailure">The inner cause of the failure.</param>
5671
public FailureDetails(Exception e, FailureDetails innerFailure)
57-
: this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false)
72+
: this(e, innerFailure, properties: null)
5873
{
5974
}
6075

@@ -63,7 +78,28 @@ public FailureDetails(Exception e, FailureDetails innerFailure)
6378
/// </summary>
6479
/// <param name="e">The exception used to generate the failure details.</param>
6580
public FailureDetails(Exception e)
66-
: this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException), false)
81+
: this(e, properties: null)
82+
{
83+
}
84+
85+
/// <summary>
86+
/// Initializes a new instance of the <see cref="FailureDetails"/> class from an exception object.
87+
/// </summary>
88+
/// <param name="e">The exception used to generate the failure details.</param>
89+
/// <param name="properties">The exception properties to include in failure details.</param>
90+
public FailureDetails(Exception e, IDictionary<string, object?>? properties)
91+
: this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException), false, properties)
92+
{
93+
}
94+
95+
/// <summary>
96+
/// Initializes a new instance of the <see cref="FailureDetails"/> class from an exception object.
97+
/// </summary>
98+
/// <param name="e">The exception used to generate the failure details.</param>
99+
/// <param name="innerFailure">The inner cause of the failure.</param>
100+
/// <param name="properties">The exception properties to include in failure details.</param>
101+
public FailureDetails(Exception e, FailureDetails innerFailure, IDictionary<string, object?>? properties)
102+
: this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false, properties)
67103
{
68104
}
69105

@@ -74,6 +110,7 @@ public FailureDetails()
74110
{
75111
this.ErrorType = "None";
76112
this.ErrorMessage = string.Empty;
113+
this.Properties = null;
77114
}
78115

79116
/// <summary>
@@ -85,6 +122,16 @@ protected FailureDetails(SerializationInfo info, StreamingContext context)
85122
this.ErrorMessage = info.GetString(nameof(this.ErrorMessage));
86123
this.StackTrace = info.GetString(nameof(this.StackTrace));
87124
this.InnerFailure = (FailureDetails)info.GetValue(nameof(this.InnerFailure), typeof(FailureDetails));
125+
// Handle backward compatibility for Properties property - defaults to null
126+
try
127+
{
128+
this.Properties = (IDictionary<string, object?>?)info.GetValue(nameof(this.Properties), typeof(IDictionary<string, object?>));
129+
}
130+
catch (SerializationException)
131+
{
132+
// Default to null for backward compatibility
133+
this.Properties = null;
134+
}
88135
}
89136

90137
/// <summary>
@@ -112,6 +159,11 @@ protected FailureDetails(SerializationInfo info, StreamingContext context)
112159
/// </summary>
113160
public bool IsNonRetriable { get; }
114161

162+
/// <summary>
163+
/// Gets additional properties associated with the failure.
164+
/// </summary>
165+
public IDictionary<string, object?>? Properties { get; }
166+
115167
/// <summary>
116168
/// Gets a debug-friendly description of the failure information.
117169
/// </summary>
@@ -204,7 +256,13 @@ static string GetErrorMessage(Exception e)
204256

205257
static FailureDetails? FromException(Exception? e)
206258
{
207-
return e == null ? null : new FailureDetails(e);
259+
return FromException(e, properties : null);
208260
}
261+
262+
static FailureDetails? FromException(Exception? e, IDictionary<string, object?>? properties)
263+
{
264+
return e == null ? null : new FailureDetails(e, properties : properties);
265+
}
266+
209267
}
210268
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// ----------------------------------------------------------------------------------
2+
// Copyright Microsoft Corporation
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ----------------------------------------------------------------------------------
13+
#nullable enable
14+
namespace DurableTask.Core
15+
{
16+
using System;
17+
using System.Collections.Generic;
18+
19+
/// <summary>
20+
/// Interface for providing custom properties from exceptions that will be included in FailureDetails.
21+
/// This interface is intended for implementation by the durabletask-dotnet layer, which will
22+
/// convert customer implementations to this interface and register them with DurableTask.Core.
23+
/// </summary>
24+
public interface IExceptionPropertiesProvider
25+
{
26+
/// <summary>
27+
/// Extracts custom properties from an exception.
28+
/// </summary>
29+
/// <param name="exception">The exception to extract properties from.</param>
30+
/// <returns>A dictionary of custom properties to include in the FailureDetails, or null if no properties should be added.</returns>
31+
IDictionary<string, object?>? GetExceptionProperties(Exception exception);
32+
}
33+
}

src/DurableTask.Core/OrchestrationContext.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ public abstract class OrchestrationContext
7373
/// </summary>
7474
internal ErrorPropagationMode ErrorPropagationMode { get; set; }
7575

76+
/// <summary>
77+
/// Gets or sets the exception properties provider that extracts custom properties from exceptions
78+
/// </summary>
79+
internal IExceptionPropertiesProvider ExceptionPropertiesProvider { get;set; }
80+
7681
/// <summary>
7782
/// Information about backend entity support, or null if the configured backend does not support entities.
7883
/// </summary>

src/DurableTask.Core/ReflectionBasedTaskActivity.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ public override async Task<string> RunAsync(TaskContext context, string input)
140140
}
141141
else
142142
{
143-
failureDetails = new FailureDetails(exception);
143+
var props = context.ExceptionPropertiesProvider.ExtractProperties(exception);
144+
failureDetails = new FailureDetails(exception, props);
144145
}
145146

146147
throw new TaskFailureException(exception.Message, exception, details)

src/DurableTask.Core/TaskActivity.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313

1414
namespace DurableTask.Core
1515
{
16-
using System;
17-
using System.Threading.Tasks;
1816
using DurableTask.Core.Common;
1917
using DurableTask.Core.Exceptions;
2018
using DurableTask.Core.Serializing;
2119
using Newtonsoft.Json.Linq;
20+
using System;
21+
using System.Threading.Tasks;
2222

2323
/// <summary>
2424
/// Base class for TaskActivity.
@@ -142,7 +142,16 @@ public override async Task<string> RunAsync(TaskContext context, string input)
142142
}
143143
else
144144
{
145-
failureDetails = new FailureDetails(e);
145+
if(context != null)
146+
{
147+
var props = context.ExceptionPropertiesProvider.ExtractProperties(e);
148+
failureDetails = new FailureDetails(e, props);
149+
}
150+
else
151+
{
152+
// Handle case for TaskContext is null.
153+
failureDetails = new FailureDetails(e);
154+
}
146155
}
147156

148157
throw new TaskFailureException(e.Message, e, details)

src/DurableTask.Core/TaskActivityDispatcher.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,31 @@ public sealed class TaskActivityDispatcher
3636
readonly DispatchMiddlewarePipeline dispatchPipeline;
3737
readonly LogHelper logHelper;
3838
readonly ErrorPropagationMode errorPropagationMode;
39+
readonly IExceptionPropertiesProvider? exceptionPropertiesProvider;
3940

41+
/// <summary>
42+
/// Initializes a new instance of the <see cref="TaskActivityDispatcher"/> class with an exception properties provider.
43+
/// </summary>
44+
/// <param name="orchestrationService">The orchestration service implementation</param>
45+
/// <param name="objectManager">The object manager for activities</param>
46+
/// <param name="dispatchPipeline">The dispatch middleware pipeline</param>
47+
/// <param name="logHelper">The log helper</param>
48+
/// <param name="errorPropagationMode">The error propagation mode</param>
49+
/// <param name="exceptionPropertiesProvider">The exception properties provider for extracting custom properties from exceptions</param>
4050
internal TaskActivityDispatcher(
4151
IOrchestrationService orchestrationService,
4252
INameVersionObjectManager<TaskActivity> objectManager,
4353
DispatchMiddlewarePipeline dispatchPipeline,
4454
LogHelper logHelper,
45-
ErrorPropagationMode errorPropagationMode)
55+
ErrorPropagationMode errorPropagationMode,
56+
IExceptionPropertiesProvider? exceptionPropertiesProvider)
4657
{
4758
this.orchestrationService = orchestrationService ?? throw new ArgumentNullException(nameof(orchestrationService));
4859
this.objectManager = objectManager ?? throw new ArgumentNullException(nameof(objectManager));
4960
this.dispatchPipeline = dispatchPipeline ?? throw new ArgumentNullException(nameof(dispatchPipeline));
5061
this.logHelper = logHelper;
5162
this.errorPropagationMode = errorPropagationMode;
63+
this.exceptionPropertiesProvider = exceptionPropertiesProvider;
5264

5365
this.dispatcher = new WorkItemDispatcher<TaskActivityWorkItem>(
5466
"TaskActivityDispatcher",
@@ -190,6 +202,7 @@ await this.dispatchPipeline.RunAsync(dispatchContext, async _ =>
190202
scheduledEvent.Version,
191203
scheduledEvent.EventId);
192204
context.ErrorPropagationMode = this.errorPropagationMode;
205+
context.ExceptionPropertiesProvider = this.exceptionPropertiesProvider;
193206

194207
HistoryEvent? responseEvent;
195208

src/DurableTask.Core/TaskContext.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,10 @@ public TaskContext(OrchestrationInstance orchestrationInstance, string name, str
6262
/// Gets or sets a value indicating how to propagate unhandled exception metadata.
6363
/// </summary>
6464
internal ErrorPropagationMode ErrorPropagationMode { get; set; }
65+
66+
/// <summary>
67+
/// Gets or sets the properties of exceptions with the provider.
68+
/// </summary>
69+
public IExceptionPropertiesProvider? ExceptionPropertiesProvider { get; set; }
6570
}
66-
}
71+
}

src/DurableTask.Core/TaskEntityDispatcher.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,31 @@ public class TaskEntityDispatcher
4242
readonly LogHelper logHelper;
4343
readonly ErrorPropagationMode errorPropagationMode;
4444
readonly TaskOrchestrationDispatcher.NonBlockingCountdownLock concurrentSessionLock;
45+
readonly IExceptionPropertiesProvider exceptionPropertiesProvider;
4546

47+
/// <summary>
48+
/// Initializes a new instance of the <see cref="TaskEntityDispatcher"/> class with an exception properties provider.
49+
/// </summary>
50+
/// <param name="orchestrationService">The orchestration service implementation</param>
51+
/// <param name="entityObjectManager">The object manager for entities</param>
52+
/// <param name="entityDispatchPipeline">The dispatch middleware pipeline</param>
53+
/// <param name="logHelper">The log helper</param>
54+
/// <param name="errorPropagationMode">The error propagation mode</param>
55+
/// <param name="exceptionPropertiesProvider">The exception properties provider for extracting custom properties from exceptions</param>
4656
internal TaskEntityDispatcher(
4757
IOrchestrationService orchestrationService,
4858
INameVersionObjectManager<TaskEntity> entityObjectManager,
4959
DispatchMiddlewarePipeline entityDispatchPipeline,
5060
LogHelper logHelper,
51-
ErrorPropagationMode errorPropagationMode)
61+
ErrorPropagationMode errorPropagationMode,
62+
IExceptionPropertiesProvider exceptionPropertiesProvider)
5263
{
5364
this.objectManager = entityObjectManager ?? throw new ArgumentNullException(nameof(entityObjectManager));
5465
this.orchestrationService = orchestrationService ?? throw new ArgumentNullException(nameof(orchestrationService));
5566
this.dispatchPipeline = entityDispatchPipeline ?? throw new ArgumentNullException(nameof(entityDispatchPipeline));
5667
this.logHelper = logHelper ?? throw new ArgumentNullException(nameof(logHelper));
5768
this.errorPropagationMode = errorPropagationMode;
69+
this.exceptionPropertiesProvider = exceptionPropertiesProvider;
5870
this.entityOrchestrationService = (orchestrationService as IEntityOrchestrationService)!;
5971
this.entityBackendProperties = entityOrchestrationService.EntityBackendProperties;
6072

src/DurableTask.Core/TaskHubWorker.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@ public TaskHubWorker(
247247
/// </remarks>
248248
public ErrorPropagationMode ErrorPropagationMode { get; set; }
249249

250+
/// <summary>
251+
/// Gets or sets the exception properties provider that extracts custom properties from exceptions
252+
/// when creating FailureDetails objects.
253+
/// </summary>
254+
public IExceptionPropertiesProvider ExceptionPropertiesProvider { get; set; }
255+
250256
/// <summary>
251257
/// Adds a middleware delegate to the orchestration dispatch pipeline.
252258
/// </summary>
@@ -296,13 +302,15 @@ public async Task<TaskHubWorker> StartAsync()
296302
this.orchestrationDispatchPipeline,
297303
this.logHelper,
298304
this.ErrorPropagationMode,
299-
this.versioningSettings);
305+
this.versioningSettings,
306+
this.ExceptionPropertiesProvider);
300307
this.activityDispatcher = new TaskActivityDispatcher(
301308
this.orchestrationService,
302309
this.activityManager,
303310
this.activityDispatchPipeline,
304311
this.logHelper,
305-
this.ErrorPropagationMode);
312+
this.ErrorPropagationMode,
313+
this.ExceptionPropertiesProvider);
306314

307315
if (this.dispatchEntitiesSeparately)
308316
{
@@ -311,7 +319,8 @@ public async Task<TaskHubWorker> StartAsync()
311319
this.entityManager,
312320
this.entityDispatchPipeline,
313321
this.logHelper,
314-
this.ErrorPropagationMode);
322+
this.ErrorPropagationMode,
323+
this.ExceptionPropertiesProvider);
315324
}
316325

317326
await this.orchestrationService.StartAsync();

0 commit comments

Comments
 (0)