Skip to content
70 changes: 67 additions & 3 deletions src/DurableTask.Core/FailureDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public FailureDetails(string errorType, string errorMessage, string? stackTrace,
/// <param name="e">The exception used to generate the failure details.</param>
/// <param name="innerFailure">The inner cause of the failure.</param>
public FailureDetails(Exception e, FailureDetails innerFailure)
: this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false)
: this(e, innerFailure, null)
{
}

Expand All @@ -63,8 +63,31 @@ public FailureDetails(Exception e, FailureDetails innerFailure)
/// </summary>
/// <param name="e">The exception used to generate the failure details.</param>
public FailureDetails(Exception e)
: this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException), false)
: this(e, (IExceptionPropertiesProvider?)null)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="FailureDetails"/> class from an exception object.
/// </summary>
/// <param name="e">The exception used to generate the failure details.</param>
/// <param name="exceptionPropertiesProvider">The provider to extract custom properties from the exception.</param>
public FailureDetails(Exception e, IExceptionPropertiesProvider? exceptionPropertiesProvider)
: this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException, exceptionPropertiesProvider), false)
{
this.Properties = GetExceptionProperties(e, exceptionPropertiesProvider);
}

/// <summary>
/// Initializes a new instance of the <see cref="FailureDetails"/> class from an exception object.
/// </summary>
/// <param name="e">The exception used to generate the failure details.</param>
/// <param name="innerFailure">The inner cause of the failure.</param>
/// <param name="exceptionPropertiesProvider">The provider to extract custom properties from the exception.</param>
public FailureDetails(Exception e, FailureDetails innerFailure, IExceptionPropertiesProvider? exceptionPropertiesProvider)
: this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false)
{
this.Properties = GetExceptionProperties(e, exceptionPropertiesProvider);
}

/// <summary>
Expand All @@ -74,6 +97,7 @@ public FailureDetails()
{
this.ErrorType = "None";
this.ErrorMessage = string.Empty;
this.Properties = null;
}

/// <summary>
Expand All @@ -85,6 +109,16 @@ protected FailureDetails(SerializationInfo info, StreamingContext context)
this.ErrorMessage = info.GetString(nameof(this.ErrorMessage));
this.StackTrace = info.GetString(nameof(this.StackTrace));
this.InnerFailure = (FailureDetails)info.GetValue(nameof(this.InnerFailure), typeof(FailureDetails));
// Handle backward compatibility for Properties property - defaults to null
try
{
this.Properties = (IDictionary<string, object>?)info.GetValue(nameof(this.Properties), typeof(IDictionary<string, object>));
}
catch (SerializationException)
{
// Default to null for backward compatibility
this.Properties = null;
}
}

/// <summary>
Expand Down Expand Up @@ -112,6 +146,11 @@ protected FailureDetails(SerializationInfo info, StreamingContext context)
/// </summary>
public bool IsNonRetriable { get; }

/// <summary>
/// Gets additional properties associated with the failure.
/// </summary>
public IDictionary<string, object>? Properties { get; }

/// <summary>
/// Gets a debug-friendly description of the failure information.
/// </summary>
Expand Down Expand Up @@ -204,7 +243,32 @@ static string GetErrorMessage(Exception e)

static FailureDetails? FromException(Exception? e)
{
return e == null ? null : new FailureDetails(e);
return FromException(e, null);
}

static FailureDetails? FromException(Exception? e, IExceptionPropertiesProvider? provider)
{
return e == null ? null : new FailureDetails(e, provider);
}

static IDictionary<string, object>? GetExceptionProperties(Exception exception, IExceptionPropertiesProvider? provider)
{
// If this is a TaskFailedException that already has FailureDetails with properties,
// use those properties instead of asking the provider
if (exception is OrchestrationException orchestrationException &&
orchestrationException.FailureDetails?.Properties != null)
{
return orchestrationException.FailureDetails.Properties;
}

if (provider == null)
{
return null;
}

// If there is a provider provided, then extract exception properties with the provider.
return provider.GetExceptionProperties(exception);
}

}
}
33 changes: 33 additions & 0 deletions src/DurableTask.Core/IExceptionPropertiesProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// ----------------------------------------------------------------------------------
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------
#nullable enable
namespace DurableTask.Core
{
using System;
using System.Collections.Generic;

/// <summary>
/// Interface for providing custom properties from exceptions that will be included in FailureDetails.
/// This interface is intended for implementation by the durabletask-dotnet layer, which will
/// convert customer implementations to this interface and register them with DurableTask.Core.
/// </summary>
public interface IExceptionPropertiesProvider
{
/// <summary>
/// Extracts custom properties from an exception.
/// </summary>
/// <param name="exception">The exception to extract properties from.</param>
/// <returns>A dictionary of custom properties to include in the FailureDetails, or null if no properties should be added.</returns>
IDictionary<string, object>? GetExceptionProperties(Exception exception);
}
}
5 changes: 5 additions & 0 deletions src/DurableTask.Core/OrchestrationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public abstract class OrchestrationContext
/// </summary>
internal ErrorPropagationMode ErrorPropagationMode { get; set; }

/// <summary>
/// Gets or sets the exception properties provider that extracts custom properties from exceptions
/// </summary>
internal IExceptionPropertiesProvider ExceptionPropertiesProvider { get;set; }

/// <summary>
/// Information about backend entity support, or null if the configured backend does not support entities.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/DurableTask.Core/ReflectionBasedTaskActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ public override async Task<string> RunAsync(TaskContext context, string input)
}
else
{
failureDetails = new FailureDetails(exception);
failureDetails = new FailureDetails(exception, context.ExceptionPropertiesProvider);
}

throw new TaskFailureException(exception.Message, exception, details)
Expand Down
2 changes: 1 addition & 1 deletion src/DurableTask.Core/TaskActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public override async Task<string> RunAsync(TaskContext context, string input)
}
else
{
failureDetails = new FailureDetails(e);
failureDetails = new FailureDetails(e, context.ExceptionPropertiesProvider);
}

throw new TaskFailureException(e.Message, e, details)
Expand Down
25 changes: 24 additions & 1 deletion src/DurableTask.Core/TaskActivityDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,41 @@ public sealed class TaskActivityDispatcher
readonly DispatchMiddlewarePipeline dispatchPipeline;
readonly LogHelper logHelper;
readonly ErrorPropagationMode errorPropagationMode;
readonly IExceptionPropertiesProvider? exceptionPropertiesProvider;

internal TaskActivityDispatcher(
IOrchestrationService orchestrationService,
INameVersionObjectManager<TaskActivity> objectManager,
DispatchMiddlewarePipeline dispatchPipeline,
LogHelper logHelper,
ErrorPropagationMode errorPropagationMode)
: this(orchestrationService, objectManager, dispatchPipeline, logHelper, errorPropagationMode, null)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="TaskActivityDispatcher"/> class with an exception properties provider.
/// </summary>
/// <param name="orchestrationService">The orchestration service implementation</param>
/// <param name="objectManager">The object manager for activities</param>
/// <param name="dispatchPipeline">The dispatch middleware pipeline</param>
/// <param name="logHelper">The log helper</param>
/// <param name="errorPropagationMode">The error propagation mode</param>
/// <param name="exceptionPropertiesProvider">The exception properties provider for extracting custom properties from exceptions</param>
internal TaskActivityDispatcher(
IOrchestrationService orchestrationService,
INameVersionObjectManager<TaskActivity> objectManager,
DispatchMiddlewarePipeline dispatchPipeline,
LogHelper logHelper,
ErrorPropagationMode errorPropagationMode,
IExceptionPropertiesProvider? exceptionPropertiesProvider)
{
this.orchestrationService = orchestrationService ?? throw new ArgumentNullException(nameof(orchestrationService));
this.objectManager = objectManager ?? throw new ArgumentNullException(nameof(objectManager));
this.dispatchPipeline = dispatchPipeline ?? throw new ArgumentNullException(nameof(dispatchPipeline));
this.logHelper = logHelper;
this.errorPropagationMode = errorPropagationMode;
this.exceptionPropertiesProvider = exceptionPropertiesProvider;

this.dispatcher = new WorkItemDispatcher<TaskActivityWorkItem>(
"TaskActivityDispatcher",
Expand Down Expand Up @@ -190,6 +212,7 @@ await this.dispatchPipeline.RunAsync(dispatchContext, async _ =>
scheduledEvent.Version,
scheduledEvent.EventId);
context.ErrorPropagationMode = this.errorPropagationMode;
context.ExceptionPropertiesProvider = this.exceptionPropertiesProvider;

HistoryEvent? responseEvent;

Expand All @@ -207,7 +230,7 @@ await this.dispatchPipeline.RunAsync(dispatchContext, async _ =>
string? details = this.IncludeDetails
? $"Unhandled exception while executing task: {e}"
: null;
responseEvent = new TaskFailedEvent(-1, scheduledEvent.EventId, e.Message, details, new FailureDetails(e));
responseEvent = new TaskFailedEvent(-1, scheduledEvent.EventId, e.Message, details, new FailureDetails(e, this.exceptionPropertiesProvider));

traceActivity?.SetStatus(ActivityStatusCode.Error, e.Message);

Expand Down
4 changes: 3 additions & 1 deletion src/DurableTask.Core/TaskContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,7 @@ public TaskContext(OrchestrationInstance orchestrationInstance, string name, str
/// Gets or sets a value indicating how to propagate unhandled exception metadata.
/// </summary>
internal ErrorPropagationMode ErrorPropagationMode { get; set; }

internal IExceptionPropertiesProvider? ExceptionPropertiesProvider { get; set; }
}
}
}
22 changes: 22 additions & 0 deletions src/DurableTask.Core/TaskEntityDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,41 @@ public class TaskEntityDispatcher
readonly LogHelper logHelper;
readonly ErrorPropagationMode errorPropagationMode;
readonly TaskOrchestrationDispatcher.NonBlockingCountdownLock concurrentSessionLock;
readonly IExceptionPropertiesProvider exceptionPropertiesProvider;

internal TaskEntityDispatcher(
IOrchestrationService orchestrationService,
INameVersionObjectManager<TaskEntity> entityObjectManager,
DispatchMiddlewarePipeline entityDispatchPipeline,
LogHelper logHelper,
ErrorPropagationMode errorPropagationMode)
: this(orchestrationService, entityObjectManager, entityDispatchPipeline, logHelper, errorPropagationMode, null)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="TaskEntityDispatcher"/> class with an exception properties provider.
/// </summary>
/// <param name="orchestrationService">The orchestration service implementation</param>
/// <param name="entityObjectManager">The object manager for entities</param>
/// <param name="entityDispatchPipeline">The dispatch middleware pipeline</param>
/// <param name="logHelper">The log helper</param>
/// <param name="errorPropagationMode">The error propagation mode</param>
/// <param name="exceptionPropertiesProvider">The exception properties provider for extracting custom properties from exceptions</param>
internal TaskEntityDispatcher(
IOrchestrationService orchestrationService,
INameVersionObjectManager<TaskEntity> entityObjectManager,
DispatchMiddlewarePipeline entityDispatchPipeline,
LogHelper logHelper,
ErrorPropagationMode errorPropagationMode,
IExceptionPropertiesProvider exceptionPropertiesProvider)
{
this.objectManager = entityObjectManager ?? throw new ArgumentNullException(nameof(entityObjectManager));
this.orchestrationService = orchestrationService ?? throw new ArgumentNullException(nameof(orchestrationService));
this.dispatchPipeline = entityDispatchPipeline ?? throw new ArgumentNullException(nameof(entityDispatchPipeline));
this.logHelper = logHelper ?? throw new ArgumentNullException(nameof(logHelper));
this.errorPropagationMode = errorPropagationMode;
this.exceptionPropertiesProvider = exceptionPropertiesProvider;
this.entityOrchestrationService = (orchestrationService as IEntityOrchestrationService)!;
this.entityBackendProperties = entityOrchestrationService.EntityBackendProperties;

Expand Down
15 changes: 12 additions & 3 deletions src/DurableTask.Core/TaskHubWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,12 @@ public TaskHubWorker(
/// </remarks>
public ErrorPropagationMode ErrorPropagationMode { get; set; }

/// <summary>
/// Gets or sets the exception properties provider that extracts custom properties from exceptions
/// when creating FailureDetails objects.
/// </summary>
public IExceptionPropertiesProvider ExceptionPropertiesProvider { get; set; }

/// <summary>
/// Adds a middleware delegate to the orchestration dispatch pipeline.
/// </summary>
Expand Down Expand Up @@ -296,13 +302,15 @@ public async Task<TaskHubWorker> StartAsync()
this.orchestrationDispatchPipeline,
this.logHelper,
this.ErrorPropagationMode,
this.versioningSettings);
this.versioningSettings,
this.ExceptionPropertiesProvider);
this.activityDispatcher = new TaskActivityDispatcher(
this.orchestrationService,
this.activityManager,
this.activityDispatchPipeline,
this.logHelper,
this.ErrorPropagationMode);
this.ErrorPropagationMode,
this.ExceptionPropertiesProvider);

if (this.dispatchEntitiesSeparately)
{
Expand All @@ -311,7 +319,8 @@ public async Task<TaskHubWorker> StartAsync()
this.entityManager,
this.entityDispatchPipeline,
this.logHelper,
this.ErrorPropagationMode);
this.ErrorPropagationMode,
this.ExceptionPropertiesProvider);
}

await this.orchestrationService.StartAsync();
Expand Down
2 changes: 1 addition & 1 deletion src/DurableTask.Core/TaskOrchestration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public override async Task<string> Execute(OrchestrationContext context, string
}
else
{
failureDetails = new FailureDetails(e);
failureDetails = new FailureDetails(e, context.ExceptionPropertiesProvider);
}

throw new OrchestrationFailureException(e.Message, details)
Expand Down
14 changes: 13 additions & 1 deletion src/DurableTask.Core/TaskOrchestrationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal class TaskOrchestrationContext : OrchestrationContext
private int idCounter;
private readonly Queue<HistoryEvent> eventsWhileSuspended;
private readonly IDictionary<int, OrchestratorAction> suspendedActionsMap;
private readonly IExceptionPropertiesProvider exceptionPropertiesProvider;

public bool IsSuspended { get; private set; }

Expand All @@ -52,6 +53,16 @@ public TaskOrchestrationContext(
TaskScheduler taskScheduler,
TaskOrchestrationEntityParameters entityParameters = null,
ErrorPropagationMode errorPropagationMode = ErrorPropagationMode.SerializeExceptions)
: this(orchestrationInstance, taskScheduler, entityParameters, errorPropagationMode, null)
{
}

public TaskOrchestrationContext(
OrchestrationInstance orchestrationInstance,
TaskScheduler taskScheduler,
TaskOrchestrationEntityParameters entityParameters,
ErrorPropagationMode errorPropagationMode,
IExceptionPropertiesProvider exceptionPropertiesProvider)
{
Utils.UnusedParameter(taskScheduler);

Expand All @@ -66,6 +77,7 @@ public TaskOrchestrationContext(
ErrorPropagationMode = errorPropagationMode;
this.eventsWhileSuspended = new Queue<HistoryEvent>();
this.suspendedActionsMap = new SortedDictionary<int, OrchestratorAction>();
this.exceptionPropertiesProvider = exceptionPropertiesProvider;
}

public IEnumerable<OrchestratorAction> OrchestratorActions => this.orchestratorActionsMap.Values;
Expand Down Expand Up @@ -684,7 +696,7 @@ public void FailOrchestration(Exception failure, OrchestrationRuntimeState runti
{
if (this.ErrorPropagationMode == ErrorPropagationMode.UseFailureDetails)
{
failureDetails = new FailureDetails(failure);
failureDetails = new FailureDetails(failure, this.exceptionPropertiesProvider);
}
else
{
Expand Down
Loading
Loading