Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -13,9 +13,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.1" />
<PackageVersion Include="Microsoft.Azure.DurableTask.AzureStorage" Version="2.6.1" />
<PackageVersion Include="Microsoft.Azure.DurableTask.Core" Version="3.5.1" />
<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 @@ -27,8 +27,9 @@
<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.1" />
<PackageVersion Include="Microsoft.DurableTask.Worker.Grpc" Version="1.16.1" />
<PackageVersion Include="Microsoft.DurableTask.Abstractions" Version="1.16.1" />
<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 All @@ -41,7 +42,6 @@
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
</ItemGroup>

<!-- Non-Production product dependencies -->
<ItemGroup>
<!-- Used by TypedInterfaces -->
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
30 changes: 22 additions & 8 deletions src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
using DurableTask.Core.Exceptions;
using DurableTask.Core.History;
using DurableTask.Core.Middleware;
using Google.Protobuf;
using Microsoft.Azure.WebJobs.Host.Executors;
using Newtonsoft.Json;
using P = Microsoft.DurableTask.Protobuf;

namespace Microsoft.Azure.WebJobs.Extensions.DurableTask
Expand Down Expand Up @@ -205,9 +205,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 +637,13 @@ private static FailureDetails GetFailureDetails(Exception e, out bool fromSerial
return details;
}

// For non-.NET language, properties at FailureDetails is not supported yet.
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: null);
}

return new FailureDetails("(unknown)", exception, stackTrace, innerFailure: null, isNonRetriable: false);
return new FailureDetails("(unknown)", exception, stackTrace, innerFailure: null, isNonRetriable: false, properties: null);
}
else
{
Expand All @@ -662,12 +663,23 @@ 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] = ProtobufUtils.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 bool TryGetRpcExceptionFields(
Expand Down Expand Up @@ -712,6 +724,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,7 +737,8 @@ private static bool TryExtractSerializedFailureDetailsFromException(string excep

int newlineIndex = exception.IndexOf('\n');
string serializedMessage = newlineIndex < 0 ? exception : exception.Substring(0, newlineIndex).Trim();
P.TaskFailureDetails? taskFailureDetails = JsonConvert.DeserializeObject<P.TaskFailureDetails>(serializedMessage);

P.TaskFailureDetails? taskFailureDetails = JsonParser.Default.Parse<P.TaskFailureDetails>(serializedMessage);
if (taskFailureDetails != null)
{
details = GetFailureDetails(taskFailureDetails);
Expand Down
168 changes: 122 additions & 46 deletions src/WebJobs.Extensions.DurableTask/ProtobufUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
#nullable enable
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
Expand All @@ -18,6 +20,8 @@
using Google.Protobuf;
using Google.Protobuf.Collections;
using Google.Protobuf.WellKnownTypes;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using P = Microsoft.DurableTask.Protobuf;

namespace Microsoft.Azure.WebJobs.Extensions.DurableTask
Expand Down Expand Up @@ -354,7 +358,8 @@ public static string Base64Encode(IMessage message)
failureDetails.ErrorMessage,
failureDetails.StackTrace,
GetFailureDetails(failureDetails.InnerFailure),
failureDetails.IsNonRetriable);
failureDetails.IsNonRetriable,
properties: ConvertProperties(failureDetails.Properties));
}

internal static P.TaskFailureDetails? GetFailureDetails(FailureDetails? failureDetails)
Expand All @@ -364,14 +369,24 @@ public static string Base64Encode(IMessage message)
return null;
}

return new P.TaskFailureDetails
var taskFailure = new P.TaskFailureDetails
{
ErrorType = failureDetails.ErrorType,
ErrorMessage = failureDetails.ErrorMessage,
StackTrace = failureDetails.StackTrace,
InnerFailure = GetFailureDetails(failureDetails.InnerFailure),
IsNonRetriable = failureDetails.IsNonRetriable,
};

if (failureDetails.Properties != null)
{
foreach (var kvp in failureDetails.Properties)
{
taskFailure.Properties[kvp.Key] = ConvertObjectToValue(kvp.Value);
}
}

return taskFailure;
}

internal static OrchestrationQuery ToOrchestrationQuery(P.QueryInstancesRequest request)
Expand Down Expand Up @@ -506,7 +521,7 @@ internal static P.PurgeInstancesResponse CreatePurgeInstancesResponse(PurgeResul
{
TraceParent = operationRequest.TraceContext.TraceParent,
TraceState = operationRequest.TraceContext.TraceState,
}
}
: null,
};
}
Expand Down Expand Up @@ -571,9 +586,9 @@ internal static P.PurgeInstancesResponse CreatePurgeInstancesResponse(PurgeResul
Name = operationAction.StartNewOrchestration.Name,
Input = operationAction.StartNewOrchestration.Input,
InstanceId = operationAction.StartNewOrchestration.InstanceId,
Version = operationAction.StartNewOrchestration.Version,
RequestTime = operationAction.StartNewOrchestration.RequestTime?.ToDateTimeOffset(),
ScheduledStartTime = operationAction.StartNewOrchestration.ScheduledTime?.ToDateTime(),
Version = operationAction.StartNewOrchestration.Version,
RequestTime = operationAction.StartNewOrchestration.RequestTime?.ToDateTimeOffset(),
ScheduledStartTime = operationAction.StartNewOrchestration.ScheduledTime?.ToDateTime(),
ParentTraceContext = operationAction.StartNewOrchestration.ParentTraceContext != null ?
new DistributedTraceContext(
operationAction.StartNewOrchestration.ParentTraceContext.TraceParent,
Expand Down Expand Up @@ -621,46 +636,107 @@ internal static P.PurgeInstancesResponse CreatePurgeInstancesResponse(PurgeResul
}
}

internal static MapField<string, Value> ConvertPocoToProtoMap(object? configurations)
{
var map = new MapField<string, Value>();

if (configurations == null)
{
return map;
}

System.Type type = configurations.GetType();
PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

foreach (PropertyInfo property in properties)
{
string propertyName = property.Name;
object? propertyValue = property.GetValue(configurations);

map[propertyName] = ConvertToProtoValue(propertyValue);
}

return map;
}

private static Value ConvertToProtoValue(object? value)
{
if (value is null)
{
return Value.ForNull();
}

return value switch
{
string s => Value.ForString(s),
bool b => Value.ForBool(b),
int i => Value.ForNumber(i),
long l => Value.ForNumber(l),
float f => Value.ForNumber(f),
double d => Value.ForNumber(d),
_ => throw new InvalidOperationException($"Unsupported type: {value.GetType()} at durable ProtobufUtils.")
};
internal static IDictionary<string, object?> ConvertProperties(MapField<string, Value> properties)
{
return properties.ToDictionary(
kvp => kvp.Key,
kvp => ConvertValueToObject(kvp.Value));
}

internal static MapField<string, Value> ConvertPocoToProtoMap(object? configurations)
{
var map = new MapField<string, Value>();

if (configurations == null)
{
return map;
}

System.Type type = configurations.GetType();
PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

foreach (PropertyInfo property in properties)
{
string propertyName = property.Name;
object? propertyValue = property.GetValue(configurations);

map[propertyName] = ConvertObjectToValue(propertyValue);
}

return map;
}

internal static Value ConvertObjectToValue(object? obj)
{
return obj switch
{
null => Value.ForNull(),
string str => Value.ForString(str),
bool b => Value.ForBool(b),
int i => Value.ForNumber(i),
long l => Value.ForNumber(l),
float f => Value.ForNumber(f),
double d => Value.ForNumber(d),
decimal dec => Value.ForNumber((double)dec),

// Handle Newtonsoft.Json types (JValue, JArray, JObject) from deserialized protobuf
JValue jv => ConvertJValueToValue(jv),
JArray ja => Value.ForList(ja.Select(ConvertObjectToValue).ToArray()),
JObject jo => Value.ForStruct(new Struct
{
Fields = { jo.Properties().ToDictionary(p => p.Name, p => ConvertObjectToValue(p.Value)) },
}),
Comment on lines +683 to +688
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of questions about this

  1. Why Newtonsoft objects specifically, and not other JSON library objects?
  2. I notice we are not doing these Newtonsoft conversions in TaskFailureDetailsConverter.cs - is this deliberate?
  3. Also, this may be generally unintuitive for customers who expect their JValue properties to be returned as JValue (or the string equivalent) and instead get some defined object.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added these because, in the test case, some nested objects such as lists and dictionaries are serialized and deserialized as JSON values when passed from Worker.Extensions to WebJobs.Extensions. Therefore, we need to include this JSON block here.

For TaskFailureDetailsConverter, there’s no serialization or deserialization involved — it directly handles the object itself, so a JSON block isn’t needed there.

Yeah, if they specifically expect a JValue, we might be violating that here — but are there really many such cases? It seems that handling nested structures is more important overall. I’m fine with either approach, though. leave it as it-is for other folks to join the discussion..


// For DateTime and DateTimeOffset, serialize to string directly.
DateTime dt => Value.ForString(dt.ToString("O")),
DateTimeOffset dto => Value.ForString(dto.ToString("O")),
IDictionary<string, object?> dict => Value.ForStruct(new Struct
{
Fields = { dict.ToDictionary(kvp => kvp.Key, kvp => ConvertObjectToValue(kvp.Value)) },
}),
IEnumerable e => Value.ForList(e.Cast<object?>().Select(ConvertObjectToValue).ToArray()),

// Fallback: convert unlisted type to string.
_ => Value.ForString(obj.ToString() ?? string.Empty),
};
}

internal static Value ConvertJValueToValue(JValue jv)
{
return jv.Type switch
{
JTokenType.Null => Value.ForNull(),
JTokenType.String => Value.ForString(jv.Value<string>() ?? string.Empty),
JTokenType.Boolean => Value.ForBool(jv.Value<bool>()),
JTokenType.Integer => Value.ForNumber(jv.Value<long>()),
JTokenType.Float => Value.ForNumber(jv.Value<double>()),
JTokenType.Date => Value.ForString($"dt:{jv.Value<DateTime>().ToString("O")}"),
_ => Value.ForString(jv.ToString()),
};
}

internal 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:
return value.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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<AssemblyName>Microsoft.Azure.WebJobs.Extensions.DurableTask</AssemblyName>
<RootNamespace>Microsoft.Azure.WebJobs.Extensions.DurableTask</RootNamespace>
<MajorVersion>3</MajorVersion>
<MinorVersion>5</MinorVersion>
<MinorVersion>6</MinorVersion>
<PatchVersion>0</PatchVersion>
<VersionPrefix>$(MajorVersion).$(MinorVersion).$(PatchVersion)</VersionPrefix>
<FileVersion>$(MajorVersion).$(MinorVersion).$(PatchVersion)</FileVersion>
Expand Down
2 changes: 1 addition & 1 deletion src/Worker.Extensions.DurableTask/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;

// TODO: Find a way to generate this dynamically at build-time
[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.DurableTask", "3.5.0")]
[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.DurableTask", "3.6.0")]
[assembly: InternalsVisibleTo("Worker.Extensions.DurableTask.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100cd1dabd5a893b40e75dc901fe7293db4a3caf9cd4d3e3ed6178d49cd476969abe74a9e0b7f4a0bb15edca48758155d35a4f05e6e852fff1b319d103b39ba04acbadd278c2753627c95e1f6f6582425374b92f51cca3deb0d2aab9de3ecda7753900a31f70a236f163006beefffe282888f85e3c76d1205ec7dfef7fa472a17b1")]
Loading
Loading