Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
aab6c60
initial commit
nytian Oct 6, 2025
646fafd
Merge branch 'dev' into nytian/failure-details
nytian Oct 7, 2025
fd80fbb
update
nytian Oct 7, 2025
b9221af
udpate
nytian Oct 13, 2025
d11dfe2
udpate
nytian Oct 13, 2025
c13e054
udpate protobuf
nytian Oct 13, 2025
2c1c8b9
update test and comment
nytian Oct 14, 2025
2ec8525
Merge branch 'dev' into nytian/failure-details
nytian Oct 14, 2025
2c15159
udpate oop
nytian Oct 14, 2025
433b061
Merge branch 'dev' into nytian/failure-details
nytian Oct 14, 2025
f5a9925
udpate
nytian Oct 14, 2025
5308af8
Merge branch 'nytian/failure-details' of https://github.com/Azure/azu…
nytian Oct 14, 2025
78aa185
Merge branch 'dev' into nytian/failure-details
nytian Oct 14, 2025
6444741
update oop
nytian Oct 15, 2025
0619cbc
Merge branch 'dev' into nytian/failure-details
nytian Oct 15, 2025
e34b063
udpate
nytian Oct 15, 2025
233fae7
Merge branch 'dev' into nytian/failure-details
nytian Oct 15, 2025
6a7893d
Merge branch 'dev' into nytian/failure-details
nytian Oct 15, 2025
eedf76f
update serialization
nytian Oct 19, 2025
b119a26
Merge branch 'nytian/failure-details' of https://github.com/Azure/azu…
nytian Oct 19, 2025
8a5f6f3
remove deleted file
nytian Oct 19, 2025
8437b45
udpate dependency version
nytian Oct 21, 2025
786d0fa
Merge branch 'dev' into nytian/failure-details
nytian Oct 22, 2025
bea2daf
Merge branch 'dev' into nytian/failure-details
nytian Oct 22, 2025
3680ce0
merge from dev
nytian Oct 23, 2025
e241200
Update Microsoft.DurableTask.Abstractions version to 1.16.0
nytian Oct 23, 2025
a4e4047
pin version
nytian Oct 23, 2025
7fbf7cd
update test
nytian Oct 23, 2025
acb4ac8
udpate test
nytian Oct 23, 2025
f645744
remove dt/dto prefix and udpate package.props
nytian Oct 24, 2025
56894a7
udpate
nytian Oct 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<PackageVersion Include="Grpc.Net.Client" Version="2.70.0" />
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.WebApiCompatShim" Version="2.2.0" />
<PackageVersion Include="Microsoft.Azure.DurableTask.ApplicationInsights" Version="0.6.0" />
<PackageVersion Include="Microsoft.Azure.DurableTask.AzureStorage" Version="2.5.0" />
<PackageVersion Include="Microsoft.Azure.DurableTask.Core" Version="3.4.0" />
<PackageVersion Include="Microsoft.Azure.DurableTask.ApplicationInsights" Version="0.7.0" />
<PackageVersion Include="Microsoft.Azure.DurableTask.AzureStorage" Version="2.6.0" />
<PackageVersion Include="Microsoft.Azure.DurableTask.Core" Version="3.5.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Core" Version="2.0.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Abstractions" Version="1.3.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs" Version="6.7.0" />
Expand All @@ -29,8 +29,8 @@
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="3.9.0" />
<PackageVersion Include="Microsoft.DurableTask.Client.Grpc" Version="1.15.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker.Grpc" Version="1.15.0" />
<PackageVersion Include="Microsoft.DurableTask.Client.Grpc" Version="1.16.0" />
<PackageVersion Include="Microsoft.DurableTask.Worker.Grpc" Version="1.16.0" />
<PackageVersion Include="Microsoft.DurableTask.Analyzers" Version="0.1.0" />
<PackageVersion Include="Microsoft.Extensions.Azure" Version="1.7.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="6.0.3" />
Expand Down Expand Up @@ -68,7 +68,7 @@
<PackageVersion Include="Microsoft.CodeAnalysis" Version="3.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="3.9.0" />
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="2.0.65" />
<PackageVersion Include="Microsoft.DurableTask.Abstractions" Version="1.15.0" />
<PackageVersion Include="Microsoft.DurableTask.Abstractions" Version="1.16.0" />
<PackageVersion Include="Microsoft.DurableTask.Generators" Version="1.0.0-preview.1" />
<PackageVersion Include="Microsoft.DurableTask.SqlServer.AzureFunctions" Version="1.5.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ message TaskFailureDetails {
google.protobuf.StringValue stackTrace = 3;
TaskFailureDetails innerFailure = 4;
bool isNonRetriable = 5;
map<string, google.protobuf.Value> properties = 6;
}

enum OrchestrationStatus {
Expand Down Expand Up @@ -469,6 +470,7 @@ message PurgeInstancesRequest {
oneof request {
string instanceId = 1;
PurgeInstanceFilter purgeInstanceFilter = 2;
InstanceBatch instanceBatch = 4;
}
bool recursive = 3;
}
Expand Down Expand Up @@ -681,8 +683,7 @@ message AbandonEntityTaskResponse {
}

message SkipGracefulOrchestrationTerminationsRequest {
// A maximum of 500 instance IDs can be provided in this list.
repeated string instanceIds = 1;
InstanceBatch instanceBatch = 1;
google.protobuf.StringValue reason = 2;
}

Expand Down Expand Up @@ -818,4 +819,9 @@ message StreamInstanceHistoryRequest {

message HistoryChunk {
repeated HistoryEvent events = 1;
}

message InstanceBatch {
// A maximum of 500 instance IDs can be provided in this list.
repeated string instanceIds = 1;
}
4 changes: 2 additions & 2 deletions src/WebJobs.Extensions.DurableTask/Grpc/Protos/versions.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# The following files were downloaded from branch main at 2025-09-17 01:59:32 UTC
https://raw.githubusercontent.com/microsoft/durabletask-protobuf/f5745e0d83f608d77871c1894d9260ceaae08967/protos/orchestrator_service.proto
# The following files were downloaded from branch main at 2025-10-13 23:40:09 UTC
https://raw.githubusercontent.com/microsoft/durabletask-protobuf/97cf9cf6ac44107b883b0f4ab1dd62ee2332cfd9/protos/orchestrator_service.proto
252 changes: 246 additions & 6 deletions src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using DurableTask.Core;
using DurableTask.Core.Entities;
Expand Down Expand Up @@ -205,9 +207,9 @@ await this.LifeCycleNotificationHelper.OrchestratorStartingAsync(
OrchestratorExecutionResult orchestratorResult;
if (functionResult.Succeeded)
{
if (workerRequiresHistory)
{
throw new SessionAbortedException("The worker has since ended the extended session and needs an orchestration history to execute the orchestration request.");
if (workerRequiresHistory)
{
throw new SessionAbortedException("The worker has since ended the extended session and needs an orchestration history to execute the orchestration request.");
}

orchestratorResult = context.GetResult();
Expand Down Expand Up @@ -637,12 +639,15 @@ private static FailureDetails GetFailureDetails(Exception e, out bool fromSerial
return details;
}

// Try to extract properties from the serialized exception JSON
IDictionary<string, object?>? properties = ExtractPropertiesFromExceptionJson(exception);

if (TrySplitExceptionTypeFromMessage(exception, out string? exceptionType, out string? exceptionMessage))
{
return new FailureDetails(exceptionType, exceptionMessage, stackTrace, innerFailure: null, isNonRetriable: false);
return new FailureDetails(exceptionType, exceptionMessage, stackTrace, innerFailure: null, isNonRetriable: false, properties: properties);
}

return new FailureDetails("(unknown)", exception, stackTrace, innerFailure: null, isNonRetriable: false);
return new FailureDetails("(unknown)", exception, stackTrace, innerFailure: null, isNonRetriable: false, properties: properties);
}
else
{
Expand All @@ -662,12 +667,68 @@ private static FailureDetails GetFailureDetails(Exception e, out bool fromSerial
return null;
}

IDictionary<string, object?>? properties = null;
if (taskFailureDetails.Properties != null && taskFailureDetails.Properties.Count > 0)
{
properties = new Dictionary<string, object?>();
foreach (var kvp in taskFailureDetails.Properties)
{
properties[kvp.Key] = ConvertValueToObject(kvp.Value);
}
}

return new FailureDetails(
taskFailureDetails.ErrorType ?? string.Empty,
taskFailureDetails.ErrorMessage ?? string.Empty,
taskFailureDetails.StackTrace,
GetFailureDetails(taskFailureDetails.InnerFailure),
taskFailureDetails.IsNonRetriable);
taskFailureDetails.IsNonRetriable,
properties);
}

private static object? ConvertValueToObject(Google.Protobuf.WellKnownTypes.Value value)
{
switch (value.KindCase)
{
case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.NullValue:
return null;
case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.NumberValue:
return value.NumberValue;
case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.StringValue:
string stringValue = value.StringValue;

// If the value starts with the 'dt:' prefix, it may represent a DateTime value — attempt to parse it.
if (stringValue.StartsWith("dt:", StringComparison.Ordinal))
{
if (DateTime.TryParse(stringValue[3..], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime date))
{
return date;
}
}

// If the value starts with the 'dto:' prefix, it may represent a DateTime value — attempt to parse it.
if (stringValue.StartsWith("dto:", StringComparison.Ordinal))
{
if (DateTimeOffset.TryParse(stringValue[4..], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset date))
{
return date;
}
}

// Otherwise just return as string
return stringValue;
case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.BoolValue:
return value.BoolValue;
case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.StructValue:
return value.StructValue.Fields.ToDictionary(
pair => pair.Key,
pair => ConvertValueToObject(pair.Value));
case Google.Protobuf.WellKnownTypes.Value.KindOneofCase.ListValue:
return value.ListValue.Values.Select(ConvertValueToObject).ToList();
default:
// Fallback: serialize the whole value to JSON string
return JsonConvert.SerializeObject(value);
}
}

private static bool TryGetRpcExceptionFields(
Expand Down Expand Up @@ -712,6 +773,7 @@ private static bool TryGetRpcExceptionFields(
return true;
}

// Parse a serialized TaskFailureDetails JSON payload embedded in an exception message.
private static bool TryExtractSerializedFailureDetailsFromException(string exception, out FailureDetails? details)
{
try
Expand All @@ -724,6 +786,20 @@ private static bool TryExtractSerializedFailureDetailsFromException(string excep

int newlineIndex = exception.IndexOf('\n');
string serializedMessage = newlineIndex < 0 ? exception : exception.Substring(0, newlineIndex).Trim();

// Manually parse JSON so Properties become native .NET types,
// This can avoid properties not to be deserialized as protobuf structs.
JsonNode? rootNode = JsonNode.Parse(serializedMessage);
if (rootNode is JsonObject rootObj)
{
details = BuildFailureDetailsFromJson(rootObj);
if (details != null)
{
return true;
}
}

// Fallback : simple deserialization.
P.TaskFailureDetails? taskFailureDetails = JsonConvert.DeserializeObject<P.TaskFailureDetails>(serializedMessage);
if (taskFailureDetails != null)
{
Expand All @@ -740,6 +816,170 @@ private static bool TryExtractSerializedFailureDetailsFromException(string excep
return false;
}

// Reconstruct a FailureDetails instance from the worker's JSON payload,
// recursively converting Properties to native .NET types.
private static FailureDetails? BuildFailureDetailsFromJson(JsonObject obj)
{
string errorType = obj["ErrorType"]?.GetValue<string>() ?? string.Empty;
string errorMessage = obj["ErrorMessage"]?.GetValue<string>() ?? string.Empty;
string? stackTrace = obj["StackTrace"]?.GetValue<string>();
bool isNonRetriable = obj["IsNonRetriable"]?.GetValue<bool>() ?? false;

// Build Properties.
IDictionary<string, object?>? properties = null;
if (obj["Properties"] is JsonObject props)
{
properties = new Dictionary<string, object?>();
foreach (var kvp in props)
{
if (kvp.Value is JsonObject value)
{
properties[kvp.Key] = ExtractValue(value);
}
}
}

// Parse innder failure details recurtively.
FailureDetails? inner = null;
if (obj["InnerFailure"] is JsonObject innerObj)
{
inner = BuildFailureDetailsFromJson(innerObj);
}

return new FailureDetails(errorType, errorMessage, stackTrace, inner, isNonRetriable, properties);
}

public static IDictionary<string, object?> ExtractPropertiesFromExceptionJson(string json)
{
var result = new Dictionary<string, object?>();

try
{
var root = JsonNode.Parse(json)?["Properties"]?.AsObject();
if (root == null)
{
return result;
}

foreach (var kvp in root)
{
var value = kvp.Value?.AsObject();
if (value == null)
{
continue;
}

result[kvp.Key] = ExtractValue(value);
}
}
catch (JsonException)
{
// If the exception string is not valid JSON (e.g., Java's toString() output),
// just return an empty properties dictionary
// We will go back here later for support including exception properties at Java.
return result;
}

return result;
}

// Convert a JSON representation of Google.Protobuf.WellKnownTypes.Value into a native .NET value.
// Handles: null, bool, number (returned as double), string (with dt:/dto: or ISO-8601 DateTime parsing),
// StructValue -> Dictionary<string, object?>, ListValue -> List<object?>.
private static object? ExtractValue(JsonObject value)
{
// Look at KindCase to determine which field is active
if (value.TryGetPropertyValue("HasStringValue", out var hasStringValue) && hasStringValue?.GetValue<bool>() == true)
{
string? s = value["StringValue"]?.GetValue<string>();
if (s is null)
{
return null;
}

if (s.StartsWith("dt:", StringComparison.Ordinal))
{
if (DateTime.TryParse(s[3..], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime dtPref))
{
return dtPref;
}
}

if (s.StartsWith("dto:", StringComparison.Ordinal))
{
if (DateTimeOffset.TryParse(s[4..], CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset dtoPref))
{
return dtoPref;
}
}

return s;
}

if (value.TryGetPropertyValue("HasNumberValue", out var hasNumValue) && hasNumValue?.GetValue<bool>() == true)
{
return value["NumberValue"]?.GetValue<double>();
}

if (value.TryGetPropertyValue("HasBoolValue", out var hasBoolValue) && hasBoolValue?.GetValue<bool>() == true)
{
return value["BoolValue"]?.GetValue<bool>();
}

if (value.TryGetPropertyValue("HasNullValue", out var hasNullValue) && hasNullValue?.GetValue<bool>() == true)
{
return null;
}

// StructValue: { "Fields": { key: {Value}, ... } }
if (value["StructValue"] is JsonObject structObj)
{
var fields = structObj["Fields"] as JsonObject;
if (fields == null)
{
return new Dictionary<string, object?>();
}

var dict = new Dictionary<string, object?>();
foreach (var field in fields)
{
if (field.Value is JsonObject fieldValueObj)
{
dict[field.Key] = ExtractValue(fieldValueObj);
}
}

return dict;
}

// ListValue: { "Values": [ {Value}, {Value}, ... ] }
if (value["ListValue"] is JsonObject listObj)
{
var values = listObj["Values"] as JsonArray;
if (values == null)
{
return new List<object?>();
}

var list = new List<object?>();
foreach (var element in values)
{
if (element is JsonObject jsonObject)
{
list.Add(ExtractValue(jsonObject));
}
else
{
list.Add(null);
}
}

return list;
}

return null;
}

private static bool TrySplitExceptionTypeFromMessage(
string exception,
[NotNullWhen(true)] out string? exceptionType,
Expand Down
Loading
Loading