Skip to content

Commit 5972863

Browse files
author
Sophia Tevosyan
committed
Merge branch 'main' into stevosyan/extended-sessions-for-orchestrations-isolated
2 parents 527cf6d + 9997696 commit 5972863

26 files changed

+1799
-59
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,6 @@ MigrationBackup/
348348

349349
# Ionide (cross platform F# VS Code tools) working folder
350350
.ionide/
351+
352+
# Rider (cross platform .NET/C# tools) working folder
353+
.idea/

CHANGELOG.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# Changelog
22

3-
## (Unreleased)
4-
5-
- Expose gRPC retry options in Azure Managed extensions ([#447](https://github.com/microsoft/durabletask-dotnet/pull/447))
3+
## v1.13.0
4+
- Add orchestration execution tracing ([#441](https://github.com/microsoft/durabletask-dotnet/pull/441))
65

76
## v1.12.0
87

98
- Activity tag support ([#426](https://github.com/microsoft/durabletask-dotnet/pull/426))
109
- Adding Analyzer to build and release ([#444](https://github.com/microsoft/durabletask-dotnet/pull/444))
1110
- Add ability to filter orchestrations at worker ([#443](https://github.com/microsoft/durabletask-dotnet/pull/443))
1211
- Removing breaking change for TaskOptions ([#446](https://github.com/microsoft/durabletask-dotnet/pull/446))
12+
- Expose gRPC retry options in Azure Managed extensions ([#447](https://github.com/microsoft/durabletask-dotnet/pull/447))
1313

1414
## v1.11.0
1515

src/Analyzers/AnalyzerReleases.Shipped.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77

88
Rule ID | Category | Severity | Notes
99
--------|----------|----------|-------
10-
DURABLE0001 | Orchestration | Warning | DateTimeOrchestrationAnalyzer
11-
DURABLE0002 | Orchestration | Warning | GuidOrchestrationAnalyzer
12-
DURABLE0003 | Orchestration | Warning | DelayOrchestrationAnalyzer
13-
DURABLE0004 | Orchestration | Warning | ThreadTaskOrchestrationAnalyzer
14-
DURABLE0005 | Orchestration | Warning | IOOrchestrationAnalyzer
15-
DURABLE0006 | Orchestration | Warning | EnvironmentOrchestrationAnalyzer
16-
DURABLE0007 | Orchestration | Warning | CancellationTokenOrchestrationAnalyzer
17-
DURABLE0008 | Orchestration | Warning | OtherBindingsOrchestrationAnalyzer
18-
DURABLE1001 | Attribute Binding | Error | OrchestrationTriggerBindingAnalyzer
19-
DURABLE1002 | Attribute Binding | Error | DurableClientBindingAnalyzer
20-
DURABLE1003 | Attribute Binding | Error | EntityTriggerBindingAnalyzer
21-
DURABLE2001 | Activity | Warning | MatchingInputOutputTypeActivityAnalyzer
22-
DURABLE2002 | Activity | Warning | MatchingInputOutputTypeActivityAnalyzer
10+
DURABLE0001 | Orchestration | Warning | **DateTimeOrchestrationAnalyzer**: Warns when non-deterministic DateTime properties like DateTime.Now, DateTime.UtcNow, or DateTime.Today are used in orchestration methods. Use context.CurrentUtcDateTime instead to ensure deterministic replay and to follow [orchestrator code constraints](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-code-constraints?tabs=csharp).
11+
DURABLE0002 | Orchestration | Warning | **GuidOrchestrationAnalyzer**: Warns when Guid.NewGuid() is used in an orchestration method. This can break determinism. Please use context.NewGuid() for orchestration-safe GUID generation to follow [orchestrator code constraints](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-code-constraints?tabs=csharp).
12+
DURABLE0003 | Orchestration | Warning | **DelayOrchestrationAnalyzer**: Warns when Task.Delay or Thread.Sleep are used in orchestrations. These APIs are non-deterministic. Please use context.CreateTimer for delays instead to follow [orchestrator code constraints](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-code-constraints?tabs=csharp).
13+
DURABLE0004 | Orchestration | Warning | **ThreadTaskOrchestrationAnalyzer**: Warns on usage of non-deterministic thread and task APIs like Thread.Start, Task.Run, Task.ContinueWith, TaskFactory.StartNew in orchestrations. Orchestrations must not use parallelism APIs as these break replay and don't follow [orchestrator code constraints](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-code-constraints?tabs=csharp).
14+
DURABLE0005 | Orchestration | Warning | **IOOrchestrationAnalyzer**: Warns when I/O APIs (e.g., HttpClient, Azure Storage clients) are used directly in orchestrations. I/O calls are not replay-safe and should be invoked via activities to follow [orchestrator code constraints](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-code-constraints?tabs=csharp)
15+
DURABLE0006 | Orchestration | Warning | **EnvironmentOrchestrationAnalyzer**: Warns on usage of System.Environment APIs (e.g., GetEnvironmentVariable) in orchestrations. Reading environment variables can introduce non-determinism. Please follow [orchestrator code constraints](https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-code-constraints?tabs=csharp)
16+
DURABLE0007 | Orchestration | Warning | **CancellationTokenOrchestrationAnalyzer**: Warns when CancellationToken parameters are used in orchestration function signatures. Orchestration methods should not accept cancellation tokens directly.
17+
DURABLE0008 | Orchestration | Warning | **OtherBindingsOrchestrationAnalyzer**: Warns when orchestration methods have input parameters with bindings other than [OrchestrationTrigger] (e.g., [EntityTrigger], [DurableClient]). Orchestrations must only use [OrchestrationTrigger] bindings.
18+
DURABLE1001 | Attribute Binding | Error | **OrchestrationTriggerBindingAnalyzer**: Ensures [OrchestrationTrigger] is only applied to parameters of type TaskOrchestrationContext.
19+
DURABLE1002 | Attribute Binding | Error | **DurableClientBindingAnalyzer**: Ensures [DurableClient] is only applied to parameters of type DurableTaskClient.
20+
DURABLE1003 | Attribute Binding | Error | **EntityTriggerBindingAnalyzer**: Ensures [EntityTrigger] is only applied to parameters of type TaskEntityDispatcher.
21+
DURABLE2001 | Activity | Warning | **MatchingInputOutputTypeActivityAnalyzer**: Warns when the input type passed to an activity invocation does not match the activity's definition.
22+
DURABLE2002 | Activity | Warning | **MatchingInputOutputTypeActivityAnalyzer**: Warns when the output type expected from an activity invocation does not match the activity's definition.

src/Client/AzureManaged/DurableTaskSchedulerClientOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ this.Credential is not null
173173
switch (authType.ToLowerInvariant())
174174
{
175175
case "defaultazure":
176-
return new DefaultAzureCredential();
176+
return new DefaultAzureCredential(); // CodeQL [SM05137] Use DefaultAzureCredential explicitly for local development and is decided by the user
177177
case "managedidentity":
178178
return new ManagedIdentityCredential(connectionString.ClientId);
179179
case "workloadidentity":

src/Client/Grpc/GrpcDurableTaskClient.cs

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Text;
66
using Google.Protobuf.WellKnownTypes;
77
using Microsoft.DurableTask.Client.Entities;
8+
using Microsoft.DurableTask.Tracing;
89
using Microsoft.Extensions.DependencyInjection;
910
using Microsoft.Extensions.Logging;
1011
using Microsoft.Extensions.Options;
@@ -107,24 +108,6 @@ public override async Task<string> ScheduleNewOrchestrationInstanceAsync(
107108
}
108109
}
109110

110-
if (Activity.Current?.Id != null || Activity.Current?.TraceStateString != null)
111-
{
112-
if (request.ParentTraceContext == null)
113-
{
114-
request.ParentTraceContext = new P.TraceContext();
115-
}
116-
117-
if (Activity.Current?.Id != null)
118-
{
119-
request.ParentTraceContext.TraceParent = Activity.Current?.Id;
120-
}
121-
122-
if (Activity.Current?.TraceStateString != null)
123-
{
124-
request.ParentTraceContext.TraceState = Activity.Current?.TraceStateString;
125-
}
126-
}
127-
128111
DateTimeOffset? startAt = options?.StartAt;
129112
this.logger.SchedulingOrchestration(
130113
request.InstanceId,
@@ -138,6 +121,8 @@ public override async Task<string> ScheduleNewOrchestrationInstanceAsync(
138121
request.ScheduledStartTimestamp = Timestamp.FromDateTimeOffset(startAt.Value.ToUniversalTime());
139122
}
140123

124+
using Activity? newActivity = TraceHelper.StartActivityForNewOrchestration(request);
125+
141126
P.CreateInstanceResponse? result = await this.sidecarClient.StartInstanceAsync(
142127
request, cancellationToken: cancellation);
143128
return result.InstanceId;
@@ -159,6 +144,8 @@ public override async Task RaiseEventAsync(
159144
Input = this.DataConverter.Serialize(eventPayload),
160145
};
161146

147+
using Activity? traceActivity = TraceHelper.StartActivityForNewEventRaisedFromClient(request, instanceId);
148+
162149
await this.sidecarClient.RaiseEventAsync(request, cancellationToken: cancellation);
163150
}
164151

src/Grpc/versions.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
# The following files were downloaded from branch stevosyan/extended-sessions-for-orchestrations-isolated at 2025-08-01 23:50:40 UTC
1+
# The following files were downloaded from branch stevosyan/extended-sessions-for-orchestrations-isolated at 2025-08-21 02:06:15 UTC
22
https://raw.githubusercontent.com/microsoft/durabletask-protobuf/930a06ef7df4f3156fdaa619e8e4d6439d43fcb5/protos/orchestrator_service.proto

src/Shared/Grpc/ProtoUtils.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Buffers;
55
using System.Buffers.Text;
6+
using System.Diagnostics;
67
using System.Diagnostics.CodeAnalysis;
78
using System.Text;
89
using DurableTask.Core;
@@ -15,6 +16,7 @@
1516
using Google.Protobuf.WellKnownTypes;
1617
using DTCore = DurableTask.Core;
1718
using P = Microsoft.DurableTask.Protobuf;
19+
using TraceHelper = Microsoft.DurableTask.Tracing.TraceHelper;
1820

1921
namespace Microsoft.DurableTask;
2022

@@ -268,29 +270,39 @@ internal static Timestamp ToTimestamp(this DateTime dateTime)
268270
/// Constructs a <see cref="P.OrchestratorResponse" />.
269271
/// </summary>
270272
/// <param name="instanceId">The orchestrator instance ID.</param>
273+
/// <param name="executionId">The orchestrator execution ID.</param>
271274
/// <param name="customStatus">The orchestrator customer status or <c>null</c> if no custom status.</param>
272275
/// <param name="actions">The orchestrator actions.</param>
273276
/// <param name="completionToken">
274277
/// The completion token for the work item. It must be the exact same <see cref="P.WorkItem.CompletionToken" />
275278
/// value that was provided by the corresponding <see cref="P.WorkItem"/> that triggered the orchestrator execution.
276279
/// </param>
277280
/// <param name="entityConversionState">The entity conversion state, or null if no conversion is required.</param>
281+
/// <param name="orchestrationActivity">The <see cref="Activity" /> that represents orchestration execution.</param>
278282
/// <param name="requiresHistory">Whether or not a history is required to complete the orchestration request and none was provided.</param>
279283
/// <returns>The orchestrator response.</returns>
280284
/// <exception cref="NotSupportedException">When an orchestrator action is unknown.</exception>
281285
internal static P.OrchestratorResponse ConstructOrchestratorResponse(
282286
string instanceId,
287+
string executionId,
283288
string? customStatus,
284289
IEnumerable<OrchestratorAction>? actions,
285290
string completionToken,
286291
EntityConversionState? entityConversionState,
292+
Activity? orchestrationActivity,
287293
bool requiresHistory = false)
288294
{
289295
var response = new P.OrchestratorResponse
290296
{
291297
InstanceId = instanceId,
292298
CustomStatus = customStatus,
293299
CompletionToken = completionToken,
300+
OrchestrationTraceContext =
301+
new()
302+
{
303+
SpanID = orchestrationActivity?.SpanId.ToString(),
304+
SpanStartTime = orchestrationActivity?.StartTimeUtc.ToTimestamp(),
305+
},
294306
RequiresHistory = requiresHistory,
295307
};
296308

@@ -305,15 +317,34 @@ internal static P.OrchestratorResponse ConstructOrchestratorResponse(
305317
{
306318
var protoAction = new P.OrchestratorAction { Id = action.Id };
307319

320+
P.TraceContext? CreateTraceContext()
321+
{
322+
if (orchestrationActivity is null)
323+
{
324+
return null;
325+
}
326+
327+
ActivitySpanId clientSpanId = ActivitySpanId.CreateRandom();
328+
ActivityContext clientActivityContext = new(orchestrationActivity.TraceId, clientSpanId, orchestrationActivity.ActivityTraceFlags, orchestrationActivity.TraceStateString);
329+
330+
return new P.TraceContext
331+
{
332+
TraceParent = $"00-{clientActivityContext.TraceId}-{clientActivityContext.SpanId}-0{clientActivityContext.TraceFlags:d}",
333+
TraceState = clientActivityContext.TraceState,
334+
};
335+
}
336+
308337
switch (action.OrchestratorActionType)
309338
{
310339
case OrchestratorActionType.ScheduleOrchestrator:
311340
var scheduleTaskAction = (ScheduleTaskOrchestratorAction)action;
341+
312342
protoAction.ScheduleTask = new P.ScheduleTaskAction
313343
{
314344
Name = scheduleTaskAction.Name,
315345
Version = scheduleTaskAction.Version,
316346
Input = scheduleTaskAction.Input,
347+
ParentTraceContext = CreateTraceContext(),
317348
};
318349

319350
if (scheduleTaskAction.Tags != null)
@@ -333,6 +364,7 @@ internal static P.OrchestratorResponse ConstructOrchestratorResponse(
333364
InstanceId = subOrchestrationAction.InstanceId,
334365
Name = subOrchestrationAction.Name,
335366
Version = subOrchestrationAction.Version,
367+
ParentTraceContext = CreateTraceContext(),
336368
};
337369
break;
338370
case OrchestratorActionType.CreateTimer:
@@ -387,6 +419,12 @@ internal static P.OrchestratorResponse ConstructOrchestratorResponse(
387419
Name = sendEventAction.EventName,
388420
Data = sendEventAction.EventData,
389421
};
422+
423+
// Distributed Tracing: start a new trace activity derived from the orchestration
424+
// for an EventRaisedEvent (external event)
425+
using Activity? traceActivity = TraceHelper.StartTraceActivityForEventRaisedFromWorker(sendEventAction, instanceId, executionId);
426+
427+
traceActivity?.Stop();
390428
}
391429

392430
break;
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Diagnostics;
5+
using System.Linq.Expressions;
6+
using System.Reflection;
7+
8+
// NOTE: Modified from https://github.com/Azure/durabletask/blob/main/src/DurableTask.Core/Tracing/DiagnosticActivityExtensions.cs
9+
namespace Microsoft.DurableTask.Tracing;
10+
11+
/// <summary>
12+
/// Replica from System.Diagnostics.DiagnosticSource >= 6.0.0.
13+
/// </summary>
14+
enum ActivityStatusCode
15+
{
16+
/// <summary>
17+
/// The default value indicating the status code is not initialized.
18+
/// </summary>
19+
Unset = 0,
20+
21+
/// <summary>
22+
/// Indicates the operation has been validated and completed successfully.
23+
/// </summary>
24+
Ok = 1,
25+
26+
/// <summary>
27+
/// Indicates an error was encountered during the operation.
28+
/// </summary>
29+
Error = 2,
30+
}
31+
32+
/// <summary>
33+
/// Extensions for <see cref="Activity"/>.
34+
/// </summary>
35+
static class DiagnosticActivityExtensions
36+
{
37+
static readonly Action<Activity, string> SetSpanIdMethod;
38+
static readonly Action<Activity, ActivityStatusCode, string> SetStatusMethod;
39+
40+
static DiagnosticActivityExtensions()
41+
{
42+
BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
43+
SetSpanIdMethod = (typeof(Activity).GetField("_spanId", flags)
44+
?? throw new InvalidOperationException("The field Activity._spanId was not found."))
45+
.CreateSetter<Activity, string>();
46+
SetStatusMethod = CreateSetStatus();
47+
}
48+
49+
/// <summary>
50+
/// Explicitly sets the span ID for the given activity.
51+
/// </summary>
52+
/// <param name="activity">The activity on which to set the span ID.</param>
53+
/// <param name="spanId">The span ID to set.</param>
54+
public static void SetSpanId(this Activity activity, string spanId)
55+
=> SetSpanIdMethod(activity, spanId);
56+
57+
/// <summary>
58+
/// Explicitly sets the status code and description for the given activity.
59+
/// </summary>
60+
/// <param name="activity">The activity on which to set the span ID.</param>
61+
/// <param name="status">The status to set.</param>
62+
/// <param name="description">The description to set.</param>
63+
public static void SetStatus(this Activity activity, ActivityStatusCode status, string description)
64+
=> SetStatusMethod(activity, status, description);
65+
66+
static Action<Activity, ActivityStatusCode, string> CreateSetStatus()
67+
{
68+
MethodInfo? method = typeof(Activity).GetMethod("SetStatus");
69+
70+
if (method is null)
71+
{
72+
return (activity, status, description) =>
73+
{
74+
#pragma warning disable CA1510
75+
if (activity is null)
76+
{
77+
throw new ArgumentNullException(nameof(activity));
78+
}
79+
#pragma warning restore CA1510
80+
81+
string? str = status switch
82+
{
83+
ActivityStatusCode.Unset => "UNSET",
84+
ActivityStatusCode.Ok => "OK",
85+
ActivityStatusCode.Error => "ERROR",
86+
_ => null,
87+
};
88+
89+
activity.SetTag("otel.status_code", str);
90+
activity.SetTag("otel.status_description", description);
91+
};
92+
}
93+
94+
/*
95+
building expression tree to effectively perform:
96+
(activity, status, description) => activity.SetStatus((ActivityStatusCode)(int)status, description);
97+
*/
98+
99+
ParameterExpression targetExp = Expression.Parameter(typeof(Activity), "target");
100+
ParameterExpression status = Expression.Parameter(typeof(ActivityStatusCode), "status");
101+
ParameterExpression description = Expression.Parameter(typeof(string), "description");
102+
UnaryExpression convert = Expression.Convert(status, typeof(int));
103+
convert = Expression.Convert(convert, method.GetParameters().First().ParameterType);
104+
MethodCallExpression callExp = Expression.Call(targetExp, method, convert, description);
105+
return Expression.Lambda<Action<Activity, ActivityStatusCode, string>>(callExp, targetExp, status, description)
106+
.Compile();
107+
}
108+
}

0 commit comments

Comments
 (0)