From 2bb20bd839d75eba8908438cb99fef9e356bf457 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Thu, 10 Jul 2025 15:09:28 -0700 Subject: [PATCH 01/40] updated to use c# built in memory cache --- src/Worker/Grpc/ExtendedSessionState.cs | 25 ++++ src/Worker/Grpc/ExtendedSessions.cs | 46 ++++++++ src/Worker/Grpc/GrpcOrchestrationRunner.cs | 127 ++++++++++++++++----- 3 files changed, 170 insertions(+), 28 deletions(-) create mode 100644 src/Worker/Grpc/ExtendedSessionState.cs create mode 100644 src/Worker/Grpc/ExtendedSessions.cs diff --git a/src/Worker/Grpc/ExtendedSessionState.cs b/src/Worker/Grpc/ExtendedSessionState.cs new file mode 100644 index 000000000..68b6e0729 --- /dev/null +++ b/src/Worker/Grpc/ExtendedSessionState.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using DurableTask.Core; + +namespace Microsoft.DurableTask.Worker.Grpc; + +/// +/// +/// +class ExtendedSessionState +{ + internal OrchestrationRuntimeState RuntimeState { get; set; } + + internal TaskOrchestration TaskOrchestration { get; set; } + + internal TaskOrchestrationExecutor OrchestrationExecutor { get; set; } + + public ExtendedSessionState(OrchestrationRuntimeState state, TaskOrchestration taskOrchestration, TaskOrchestrationExecutor orchestrationExecutor) + { + RuntimeState = state; + TaskOrchestration = taskOrchestration; + OrchestrationExecutor = orchestrationExecutor; + } +} diff --git a/src/Worker/Grpc/ExtendedSessions.cs b/src/Worker/Grpc/ExtendedSessions.cs new file mode 100644 index 000000000..fbd8816ac --- /dev/null +++ b/src/Worker/Grpc/ExtendedSessions.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; + +namespace Microsoft.DurableTask.Worker.Grpc; + +public class ExtendedSessions +{ + readonly Dictionary extendedSessions = []; + readonly object extendedSessionsLock = new object(); + readonly int extendedSessionIdleTimeoutInSeconds; + + internal void Add(string instanceId, ExtendedSessionState sessionState) + { + lock (this.extendedSessionsLock) + { + this.extendedSessions[instanceId] = sessionState; + } + } + + internal void Remove(string instanceId) + { + lock (this.extendedSessionsLock) + { + this.extendedSessions.Remove(instanceId); + } + } + + internal bool TryGetValue(string instanceId, out ExtendedSessionState? sessionState) + { + lock (this.extendedSessionsLock) + { + bool success = this.extendedSessions.TryGetValue(instanceId, out sessionState); + + return success; + } + } + + void Purge() + { + } +} diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index 9a6e3c5fc..c0d6fcb6f 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -1,10 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using DurableTask.Core; +using DurableTask.Core; +using DurableTask.Core.Exceptions; using DurableTask.Core.History; using Google.Protobuf; -using Microsoft.DurableTask.Worker.Shims; +using Microsoft.DurableTask.Worker.Shims; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using P = Microsoft.DurableTask.Protobuf; @@ -23,7 +25,9 @@ namespace Microsoft.DurableTask.Worker.Grpc; /// /// public static class GrpcOrchestrationRunner -{ +{ + const int DefaultExtendedSessionIdleTimeoutInSeconds = 30; + /// /// Loads orchestration history from and uses it to execute the /// orchestrator function code pointed to by . @@ -37,7 +41,10 @@ public static class GrpcOrchestrationRunner /// /// The base64-encoded protobuf payload representing an orchestration execution request. /// - /// A function that implements the orchestrator logic. + /// A function that implements the orchestrator logic. + /// + /// The cache of extended sessions which can be used to retrieve the if this orchestration request is from within an extended session. + /// /// /// Optional from which injected dependencies can be retrieved. /// @@ -49,11 +56,12 @@ public static class GrpcOrchestrationRunner /// public static string LoadAndRun( string encodedOrchestratorRequest, - Func> orchestratorFunc, + Func> orchestratorFunc, + IMemoryCache extendedSessions, IServiceProvider? services = null) { Check.NotNull(orchestratorFunc); - return LoadAndRun(encodedOrchestratorRequest, FuncTaskOrchestrator.Create(orchestratorFunc), services); + return LoadAndRun(encodedOrchestratorRequest, FuncTaskOrchestrator.Create(orchestratorFunc), extendedSessions, services); } /// @@ -65,6 +73,9 @@ public static string LoadAndRun( /// /// /// An implementation that defines the orchestrator logic. + /// + /// + /// The cache of extended sessions which can be used to retrieve the if this orchestration request is from within an extended session. /// /// /// Optional from which injected dependencies can be retrieved. @@ -80,7 +91,8 @@ public static string LoadAndRun( /// public static string LoadAndRun( string encodedOrchestratorRequest, - ITaskOrchestrator implementation, + ITaskOrchestrator implementation, + IMemoryCache extendedSessions, IServiceProvider? services = null) { Check.NotNullOrEmpty(encodedOrchestratorRequest); @@ -93,27 +105,86 @@ public static string LoadAndRun( IEnumerable newEvents = request.NewEvents.Select(ProtoUtils.ConvertHistoryEvent); Dictionary properties = request.Properties.ToDictionary( pair => pair.Key, - pair => ProtoUtils.ConvertValueToObject(pair.Value)); - - // Re-construct the orchestration state from the history. - // New events must be added using the AddEvent method. - OrchestrationRuntimeState runtimeState = new(pastEvents); - foreach (HistoryEvent newEvent in newEvents) - { - runtimeState.AddEvent(newEvent); - } - - TaskName orchestratorName = new(runtimeState.Name); - ParentOrchestrationInstance? parent = runtimeState.ParentInstance is ParentInstance p - ? new(new(p.Name), p.OrchestrationInstance.InstanceId) - : null; - - DurableTaskShimFactory factory = services is null - ? DurableTaskShimFactory.Default - : ActivatorUtilities.GetServiceOrCreateInstance(services); - TaskOrchestration shim = factory.CreateOrchestration(orchestratorName, implementation, properties, parent); - TaskOrchestrationExecutor executor = new(runtimeState, shim, BehaviorOnContinueAsNew.Carryover, request.EntityParameters.ToCore(), ErrorPropagationMode.UseFailureDetails); - OrchestratorExecutionResult result = executor.Execute(); + pair => ProtoUtils.ConvertValueToObject(pair.Value)); + + OrchestratorExecutionResult? result = null; + bool addToExtendedSessions = false; + if (properties.TryGetValue("ExtendedSession", out object? extendedSession) + && extendedSession is bool isExtendedSession && isExtendedSession) + { + if (extendedSessions.TryGetValue(request.InstanceId, out ExtendedSessionState? extendedSessionState)) + { + OrchestrationRuntimeState runtimeState = extendedSessionState!.RuntimeState; + foreach (HistoryEvent newEvent in newEvents) + { + runtimeState.AddEvent(newEvent); + } + + result = extendedSessionState.OrchestrationExecutor.ExecuteNewEvents(); + if (extendedSessionState.OrchestrationExecutor.IsCompleted) + { + extendedSessions.Remove(request.InstanceId); + } + } + else + { + addToExtendedSessions = true; + } + } + + if (result == null) + { + if (!properties.TryGetValue("ExtendedSessionIdleTimeoutInSeconds", out object? extendedSessionIdleTimeout) + || extendedSessionIdleTimeout is not int extendedSessionIdleTimeoutInSeconds + || extendedSessionIdleTimeoutInSeconds <= 0) + { + extendedSessionIdleTimeoutInSeconds = DefaultExtendedSessionIdleTimeoutInSeconds; + } + + // If this is the first orchestration execution, then the past events count will be 0 but includePastEvents will be true (there are just none to include). + // Otherwise, there is an orchestration history but DurableTask.Core did not attach it since the extended session is still active on its end, but we have since evicted the + // session and lost the orchestration history so we cannot replay the orchestration. + // We will fail the session, in which case the work item will be retried with a history attached. + if (pastEvents.Count == 0 + && (properties.TryGetValue("IncludePastEvents", out object? includePastEvents) + && includePastEvents is bool pastEventsIncluded && !pastEventsIncluded)) + { + throw new SessionAbortedException($"The worker has since ended the extended session due to its being idle longer than the maximum of {extendedSessionIdleTimeoutInSeconds} seconds."); + } + + // Re-construct the orchestration state from the history. + // New events must be added using the AddEvent method. + OrchestrationRuntimeState runtimeState = new(pastEvents); + + foreach (HistoryEvent newEvent in newEvents) + { + runtimeState.AddEvent(newEvent); + } + + TaskName orchestratorName = new(runtimeState.Name); + ParentOrchestrationInstance? parent = runtimeState.ParentInstance is ParentInstance p + ? new(new(p.Name), p.OrchestrationInstance.InstanceId) + : null; + + DurableTaskShimFactory factory = services is null + ? DurableTaskShimFactory.Default + : ActivatorUtilities.GetServiceOrCreateInstance(services); + TaskOrchestration shim = factory.CreateOrchestration(orchestratorName, implementation, properties, parent); + TaskOrchestrationExecutor executor = new(runtimeState, shim, BehaviorOnContinueAsNew.Carryover, request.EntityParameters.ToCore(), ErrorPropagationMode.UseFailureDetails); + result = executor.Execute(); + + if (addToExtendedSessions && !executor.IsCompleted) + { + extendedSessions.Set( + request.InstanceId, + new(runtimeState, shim, executor), + new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(extendedSessionIdleTimeoutInSeconds) }); + } + else + { + extendedSessions.Remove(request.InstanceId); + } + } P.OrchestratorResponse response = ProtoUtils.ConstructOrchestratorResponse( request.InstanceId, From afde88d637473d1f83dc9e627a1ded994a681be0 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 11 Jul 2025 14:31:11 -0700 Subject: [PATCH 02/40] fixed bug where new events were not cleared --- src/Worker/Grpc/GrpcOrchestrationRunner.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index c0d6fcb6f..6132bcb0b 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -115,6 +115,7 @@ public static string LoadAndRun( if (extendedSessions.TryGetValue(request.InstanceId, out ExtendedSessionState? extendedSessionState)) { OrchestrationRuntimeState runtimeState = extendedSessionState!.RuntimeState; + runtimeState.NewEvents.Clear(); foreach (HistoryEvent newEvent in newEvents) { runtimeState.AddEvent(newEvent); From 5001bb7c14f8bd39fe197feccf1a48a5d599378f Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 14 Jul 2025 13:31:17 -0700 Subject: [PATCH 03/40] added a new field to the OrchestratorResponse, restored the old public facing method without the extended sessions parameter --- src/Shared/Grpc/ProtoUtils.cs | 235 +++++++++++---------- src/Worker/Grpc/GrpcOrchestrationRunner.cs | 219 ++++++++++++------- 2 files changed, 264 insertions(+), 190 deletions(-) diff --git a/src/Shared/Grpc/ProtoUtils.cs b/src/Shared/Grpc/ProtoUtils.cs index 868ecc661..8c47c9105 100644 --- a/src/Shared/Grpc/ProtoUtils.cs +++ b/src/Shared/Grpc/ProtoUtils.cs @@ -275,160 +275,167 @@ internal static Timestamp ToTimestamp(this DateTime dateTime) /// value that was provided by the corresponding that triggered the orchestrator execution. /// /// The entity conversion state, or null if no conversion is required. + /// Whether or not a history is required to complete the orchestration request and none was provided. /// The orchestrator response. /// When an orchestrator action is unknown. internal static P.OrchestratorResponse ConstructOrchestratorResponse( string instanceId, string? customStatus, - IEnumerable actions, + IEnumerable? actions, string completionToken, - EntityConversionState? entityConversionState) + EntityConversionState? entityConversionState, + bool requiresHistory = false) { - Check.NotNull(actions); var response = new P.OrchestratorResponse { InstanceId = instanceId, CustomStatus = customStatus, CompletionToken = completionToken, + RequiresHistory = requiresHistory, }; - foreach (OrchestratorAction action in actions) + // If a history is required and the orchestration request was not completed, then there is no list of actions. + if (!requiresHistory) { - var protoAction = new P.OrchestratorAction { Id = action.Id }; - - switch (action.OrchestratorActionType) + Check.NotNull(actions); + foreach (OrchestratorAction action in actions) { - case OrchestratorActionType.ScheduleOrchestrator: - var scheduleTaskAction = (ScheduleTaskOrchestratorAction)action; - protoAction.ScheduleTask = new P.ScheduleTaskAction - { - Name = scheduleTaskAction.Name, - Version = scheduleTaskAction.Version, - Input = scheduleTaskAction.Input, - }; + var protoAction = new P.OrchestratorAction { Id = action.Id }; - if (scheduleTaskAction.Tags != null) - { - foreach (KeyValuePair tag in scheduleTaskAction.Tags) + switch (action.OrchestratorActionType) + { + case OrchestratorActionType.ScheduleOrchestrator: + var scheduleTaskAction = (ScheduleTaskOrchestratorAction)action; + protoAction.ScheduleTask = new P.ScheduleTaskAction { - protoAction.ScheduleTask.Tags[tag.Key] = tag.Value; + Name = scheduleTaskAction.Name, + Version = scheduleTaskAction.Version, + Input = scheduleTaskAction.Input, + }; + + if (scheduleTaskAction.Tags != null) + { + foreach (KeyValuePair tag in scheduleTaskAction.Tags) + { + protoAction.ScheduleTask.Tags[tag.Key] = tag.Value; + } } - } - break; - case OrchestratorActionType.CreateSubOrchestration: - var subOrchestrationAction = (CreateSubOrchestrationAction)action; - protoAction.CreateSubOrchestration = new P.CreateSubOrchestrationAction - { - Input = subOrchestrationAction.Input, - InstanceId = subOrchestrationAction.InstanceId, - Name = subOrchestrationAction.Name, - Version = subOrchestrationAction.Version, - }; - break; - case OrchestratorActionType.CreateTimer: - var createTimerAction = (CreateTimerOrchestratorAction)action; - protoAction.CreateTimer = new P.CreateTimerAction - { - FireAt = createTimerAction.FireAt.ToTimestamp(), - }; - break; - case OrchestratorActionType.SendEvent: - var sendEventAction = (SendEventOrchestratorAction)action; - if (sendEventAction.Instance == null) - { - throw new ArgumentException( - $"{nameof(SendEventOrchestratorAction)} cannot have a null Instance property!"); - } - - if (entityConversionState is not null - && DTCore.Common.Entities.IsEntityInstance(sendEventAction.Instance.InstanceId) - && sendEventAction.EventName is not null - && sendEventAction.EventData is not null) - { - P.SendEntityMessageAction sendAction = new P.SendEntityMessageAction(); - protoAction.SendEntityMessage = sendAction; + break; + case OrchestratorActionType.CreateSubOrchestration: + var subOrchestrationAction = (CreateSubOrchestrationAction)action; + protoAction.CreateSubOrchestration = new P.CreateSubOrchestrationAction + { + Input = subOrchestrationAction.Input, + InstanceId = subOrchestrationAction.InstanceId, + Name = subOrchestrationAction.Name, + Version = subOrchestrationAction.Version, + }; + break; + case OrchestratorActionType.CreateTimer: + var createTimerAction = (CreateTimerOrchestratorAction)action; + protoAction.CreateTimer = new P.CreateTimerAction + { + FireAt = createTimerAction.FireAt.ToTimestamp(), + }; + break; + case OrchestratorActionType.SendEvent: + var sendEventAction = (SendEventOrchestratorAction)action; + if (sendEventAction.Instance == null) + { + throw new ArgumentException( + $"{nameof(SendEventOrchestratorAction)} cannot have a null Instance property!"); + } - EntityConversions.DecodeEntityMessageAction( - sendEventAction.EventName, - sendEventAction.EventData, - sendEventAction.Instance.InstanceId, - sendAction, - out string requestId); + if (entityConversionState is not null + && DTCore.Common.Entities.IsEntityInstance(sendEventAction.Instance.InstanceId) + && sendEventAction.EventName is not null + && sendEventAction.EventData is not null) + { + P.SendEntityMessageAction sendAction = new P.SendEntityMessageAction(); + protoAction.SendEntityMessage = sendAction; - entityConversionState.EntityRequestIds.Add(requestId); + EntityConversions.DecodeEntityMessageAction( + sendEventAction.EventName, + sendEventAction.EventData, + sendEventAction.Instance.InstanceId, + sendAction, + out string requestId); - switch (sendAction.EntityMessageTypeCase) - { - case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityLockRequested: - entityConversionState.AddUnlockObligations(sendAction.EntityLockRequested); - break; - case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityUnlockSent: - entityConversionState.RemoveUnlockObligation(sendAction.EntityUnlockSent.TargetInstanceId); - break; - default: - break; + entityConversionState.EntityRequestIds.Add(requestId); + + switch (sendAction.EntityMessageTypeCase) + { + case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityLockRequested: + entityConversionState.AddUnlockObligations(sendAction.EntityLockRequested); + break; + case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityUnlockSent: + entityConversionState.RemoveUnlockObligation(sendAction.EntityUnlockSent.TargetInstanceId); + break; + default: + break; + } } - } - else - { - protoAction.SendEvent = new P.SendEventAction + else { - Instance = sendEventAction.Instance.ToProtobuf(), - Name = sendEventAction.EventName, - Data = sendEventAction.EventData, - }; - } + protoAction.SendEvent = new P.SendEventAction + { + Instance = sendEventAction.Instance.ToProtobuf(), + Name = sendEventAction.EventName, + Data = sendEventAction.EventData, + }; + } - break; - case OrchestratorActionType.OrchestrationComplete: + break; + case OrchestratorActionType.OrchestrationComplete: - if (entityConversionState is not null) - { - // as a precaution, unlock any entities that were not unlocked for some reason, before - // completing the orchestration. - foreach ((string target, string criticalSectionId) in entityConversionState.ResetObligations()) + if (entityConversionState is not null) { - response.Actions.Add(new P.OrchestratorAction + // as a precaution, unlock any entities that were not unlocked for some reason, before + // completing the orchestration. + foreach ((string target, string criticalSectionId) in entityConversionState.ResetObligations()) { - Id = action.Id, - SendEntityMessage = new P.SendEntityMessageAction + response.Actions.Add(new P.OrchestratorAction { - EntityUnlockSent = new P.EntityUnlockSentEvent + Id = action.Id, + SendEntityMessage = new P.SendEntityMessageAction { - CriticalSectionId = criticalSectionId, - TargetInstanceId = target, - ParentInstanceId = entityConversionState.CurrentInstance?.InstanceId, + EntityUnlockSent = new P.EntityUnlockSentEvent + { + CriticalSectionId = criticalSectionId, + TargetInstanceId = target, + ParentInstanceId = entityConversionState.CurrentInstance?.InstanceId, + }, }, - }, - }); + }); + } } - } - var completeAction = (OrchestrationCompleteOrchestratorAction)action; - protoAction.CompleteOrchestration = new P.CompleteOrchestrationAction - { - CarryoverEvents = + var completeAction = (OrchestrationCompleteOrchestratorAction)action; + protoAction.CompleteOrchestration = new P.CompleteOrchestrationAction + { + CarryoverEvents = { // TODO }, - Details = completeAction.Details, - NewVersion = completeAction.NewVersion, - OrchestrationStatus = completeAction.OrchestrationStatus.ToProtobuf(), - Result = completeAction.Result, - }; + Details = completeAction.Details, + NewVersion = completeAction.NewVersion, + OrchestrationStatus = completeAction.OrchestrationStatus.ToProtobuf(), + Result = completeAction.Result, + }; - if (completeAction.OrchestrationStatus == OrchestrationStatus.Failed) - { - protoAction.CompleteOrchestration.FailureDetails = completeAction.FailureDetails.ToProtobuf(); - } + if (completeAction.OrchestrationStatus == OrchestrationStatus.Failed) + { + protoAction.CompleteOrchestration.FailureDetails = completeAction.FailureDetails.ToProtobuf(); + } - break; - default: - throw new NotSupportedException($"Unknown orchestrator action: {action.OrchestratorActionType}"); - } + break; + default: + throw new NotSupportedException($"Unknown orchestrator action: {action.OrchestratorActionType}"); + } - response.Actions.Add(protoAction); + response.Actions.Add(protoAction); + } } return response; diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index 6132bcb0b..7c2fd0493 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -26,42 +26,105 @@ namespace Microsoft.DurableTask.Worker.Grpc; /// public static class GrpcOrchestrationRunner { - const int DefaultExtendedSessionIdleTimeoutInSeconds = 30; - - /// - /// Loads orchestration history from and uses it to execute the - /// orchestrator function code pointed to by . - /// - /// - /// The type of the orchestrator function input. This type must be deserializable from JSON. - /// - /// - /// The type of the orchestrator function output. This type must be serializable to JSON. - /// - /// - /// The base64-encoded protobuf payload representing an orchestration execution request. - /// + /// + /// Loads orchestration history from and uses it to execute the + /// orchestrator function code pointed to by . + /// + /// + /// The type of the orchestrator function input. This type must be deserializable from JSON. + /// + /// + /// The type of the orchestrator function output. This type must be serializable to JSON. + /// + /// + /// The base64-encoded protobuf payload representing an orchestration execution request. + /// /// A function that implements the orchestrator logic. - /// - /// The cache of extended sessions which can be used to retrieve the if this orchestration request is from within an extended session. - /// - /// - /// Optional from which injected dependencies can be retrieved. - /// - /// - /// Returns a base64-encoded set of orchestrator actions to be interpreted by the external orchestration engine. - /// - /// - /// Thrown if or is null. - /// - public static string LoadAndRun( - string encodedOrchestratorRequest, + /// + /// Optional from which injected dependencies can be retrieved. + /// + /// + /// Returns a base64-encoded set of orchestrator actions to be interpreted by the external orchestration engine. + /// + /// + /// Thrown if or is null. + /// + public static string LoadAndRun( + string encodedOrchestratorRequest, Func> orchestratorFunc, - IMemoryCache extendedSessions, - IServiceProvider? services = null) - { - Check.NotNull(orchestratorFunc); - return LoadAndRun(encodedOrchestratorRequest, FuncTaskOrchestrator.Create(orchestratorFunc), extendedSessions, services); + IServiceProvider? services = null) + { + Check.NotNull(orchestratorFunc); + return LoadAndRun(encodedOrchestratorRequest, FuncTaskOrchestrator.Create(orchestratorFunc), services); + } + + /// + /// Deserializes orchestration history from and uses it to resume the + /// orchestrator implemented by . + /// + /// + /// The encoded protobuf payload representing an orchestration execution request. This is a base64-encoded string. + /// + /// + /// An implementation that defines the orchestrator logic. + /// + /// + /// Optional from which injected dependencies can be retrieved. + /// + /// + /// Returns a serialized set of orchestrator actions that should be used as the return value of the orchestrator function trigger. + /// + /// + /// Thrown if or is null. + /// + /// + /// Thrown if contains invalid data. + /// + public static string LoadAndRun( + string encodedOrchestratorRequest, + ITaskOrchestrator implementation, + IServiceProvider? services = null) + { + Check.NotNullOrEmpty(encodedOrchestratorRequest); + Check.NotNull(implementation); + + P.OrchestratorRequest request = P.OrchestratorRequest.Parser.Base64Decode( + encodedOrchestratorRequest); + + List pastEvents = request.PastEvents.Select(ProtoUtils.ConvertHistoryEvent).ToList(); + IEnumerable newEvents = request.NewEvents.Select(ProtoUtils.ConvertHistoryEvent); + Dictionary properties = request.Properties.ToDictionary( + pair => pair.Key, + pair => ProtoUtils.ConvertValueToObject(pair.Value)); + + // Re-construct the orchestration state from the history. + // New events must be added using the AddEvent method. + OrchestrationRuntimeState runtimeState = new(pastEvents); + foreach (HistoryEvent newEvent in newEvents) + { + runtimeState.AddEvent(newEvent); + } + + TaskName orchestratorName = new(runtimeState.Name); + ParentOrchestrationInstance? parent = runtimeState.ParentInstance is ParentInstance p + ? new(new(p.Name), p.OrchestrationInstance.InstanceId) + : null; + + DurableTaskShimFactory factory = services is null + ? DurableTaskShimFactory.Default + : ActivatorUtilities.GetServiceOrCreateInstance(services); + TaskOrchestration shim = factory.CreateOrchestration(orchestratorName, implementation, properties, parent); + TaskOrchestrationExecutor executor = new(runtimeState, shim, BehaviorOnContinueAsNew.Carryover, request.EntityParameters.ToCore(), ErrorPropagationMode.UseFailureDetails); + OrchestratorExecutionResult result = executor.Execute(); + + P.OrchestratorResponse response = ProtoUtils.ConstructOrchestratorResponse( + request.InstanceId, + result.CustomStatus, + result.Actions, + completionToken: string.Empty, /* doesn't apply */ + entityConversionState: null); + byte[] responseBytes = response.ToByteArray(); + return Convert.ToBase64String(responseBytes); } /// @@ -109,8 +172,8 @@ public static string LoadAndRun( OrchestratorExecutionResult? result = null; bool addToExtendedSessions = false; - if (properties.TryGetValue("ExtendedSession", out object? extendedSession) - && extendedSession is bool isExtendedSession && isExtendedSession) + bool requiresHistory = false; + if (properties.TryGetValue("ExtendedSession", out object? isExtendedSession) && (bool)isExtendedSession!) { if (extendedSessions.TryGetValue(request.InstanceId, out ExtendedSessionState? extendedSessionState)) { @@ -135,64 +198,68 @@ public static string LoadAndRun( if (result == null) { - if (!properties.TryGetValue("ExtendedSessionIdleTimeoutInSeconds", out object? extendedSessionIdleTimeout) - || extendedSessionIdleTimeout is not int extendedSessionIdleTimeoutInSeconds - || extendedSessionIdleTimeoutInSeconds <= 0) + double extendedSessionIdleTimeoutInSeconds = 0; + if (properties.TryGetValue("ExtendedSessionIdleTimeoutInSeconds", out object? extendedSessionIdleTimeout) + && (double)extendedSessionIdleTimeout! >= 0) { - extendedSessionIdleTimeoutInSeconds = DefaultExtendedSessionIdleTimeoutInSeconds; + extendedSessionIdleTimeoutInSeconds = (double)extendedSessionIdleTimeout!; + } + else + { + addToExtendedSessions = false; } // If this is the first orchestration execution, then the past events count will be 0 but includePastEvents will be true (there are just none to include). // Otherwise, there is an orchestration history but DurableTask.Core did not attach it since the extended session is still active on its end, but we have since evicted the // session and lost the orchestration history so we cannot replay the orchestration. - // We will fail the session, in which case the work item will be retried with a history attached. - if (pastEvents.Count == 0 - && (properties.TryGetValue("IncludePastEvents", out object? includePastEvents) - && includePastEvents is bool pastEventsIncluded && !pastEventsIncluded)) + if (pastEvents.Count == 0 && (properties.TryGetValue("IncludePastEvents", out object? pastEventsIncluded) && !(bool)pastEventsIncluded!)) { - throw new SessionAbortedException($"The worker has since ended the extended session due to its being idle longer than the maximum of {extendedSessionIdleTimeoutInSeconds} seconds."); + requiresHistory = true; } - - // Re-construct the orchestration state from the history. - // New events must be added using the AddEvent method. - OrchestrationRuntimeState runtimeState = new(pastEvents); - - foreach (HistoryEvent newEvent in newEvents) + else { - runtimeState.AddEvent(newEvent); - } + // Re-construct the orchestration state from the history. + // New events must be added using the AddEvent method. + OrchestrationRuntimeState runtimeState = new(pastEvents); - TaskName orchestratorName = new(runtimeState.Name); - ParentOrchestrationInstance? parent = runtimeState.ParentInstance is ParentInstance p - ? new(new(p.Name), p.OrchestrationInstance.InstanceId) - : null; + foreach (HistoryEvent newEvent in newEvents) + { + runtimeState.AddEvent(newEvent); + } - DurableTaskShimFactory factory = services is null - ? DurableTaskShimFactory.Default - : ActivatorUtilities.GetServiceOrCreateInstance(services); - TaskOrchestration shim = factory.CreateOrchestration(orchestratorName, implementation, properties, parent); - TaskOrchestrationExecutor executor = new(runtimeState, shim, BehaviorOnContinueAsNew.Carryover, request.EntityParameters.ToCore(), ErrorPropagationMode.UseFailureDetails); - result = executor.Execute(); + TaskName orchestratorName = new(runtimeState.Name); + ParentOrchestrationInstance? parent = runtimeState.ParentInstance is ParentInstance p + ? new(new(p.Name), p.OrchestrationInstance.InstanceId) + : null; - if (addToExtendedSessions && !executor.IsCompleted) - { - extendedSessions.Set( - request.InstanceId, - new(runtimeState, shim, executor), - new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(extendedSessionIdleTimeoutInSeconds) }); - } - else - { - extendedSessions.Remove(request.InstanceId); + DurableTaskShimFactory factory = services is null + ? DurableTaskShimFactory.Default + : ActivatorUtilities.GetServiceOrCreateInstance(services); + TaskOrchestration shim = factory.CreateOrchestration(orchestratorName, implementation, properties, parent); + TaskOrchestrationExecutor executor = new(runtimeState, shim, BehaviorOnContinueAsNew.Carryover, request.EntityParameters.ToCore(), ErrorPropagationMode.UseFailureDetails); + result = executor.Execute(); + + if (addToExtendedSessions && !executor.IsCompleted) + { + extendedSessions.Set( + request.InstanceId, + new(runtimeState, shim, executor), + new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(extendedSessionIdleTimeoutInSeconds) }); + } + else + { + extendedSessions.Remove(request.InstanceId); + } } } P.OrchestratorResponse response = ProtoUtils.ConstructOrchestratorResponse( request.InstanceId, - result.CustomStatus, - result.Actions, + result?.CustomStatus, + result?.Actions, completionToken: string.Empty, /* doesn't apply */ - entityConversionState: null); + entityConversionState: null, + requiresHistory); byte[] responseBytes = response.ToByteArray(); return Convert.ToBase64String(responseBytes); } From 38d3a3255a5e03e55f926abbff73b8364d25ad10 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 14 Jul 2025 13:55:04 -0700 Subject: [PATCH 04/40] removed unused ExtendedSessions object --- src/Worker/Grpc/ExtendedSessions.cs | 46 ----------------------------- 1 file changed, 46 deletions(-) delete mode 100644 src/Worker/Grpc/ExtendedSessions.cs diff --git a/src/Worker/Grpc/ExtendedSessions.cs b/src/Worker/Grpc/ExtendedSessions.cs deleted file mode 100644 index fbd8816ac..000000000 --- a/src/Worker/Grpc/ExtendedSessions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Text; - -namespace Microsoft.DurableTask.Worker.Grpc; - -public class ExtendedSessions -{ - readonly Dictionary extendedSessions = []; - readonly object extendedSessionsLock = new object(); - readonly int extendedSessionIdleTimeoutInSeconds; - - internal void Add(string instanceId, ExtendedSessionState sessionState) - { - lock (this.extendedSessionsLock) - { - this.extendedSessions[instanceId] = sessionState; - } - } - - internal void Remove(string instanceId) - { - lock (this.extendedSessionsLock) - { - this.extendedSessions.Remove(instanceId); - } - } - - internal bool TryGetValue(string instanceId, out ExtendedSessionState? sessionState) - { - lock (this.extendedSessionsLock) - { - bool success = this.extendedSessions.TryGetValue(instanceId, out sessionState); - - return success; - } - } - - void Purge() - { - } -} From 92bb2cd555b19e44a89de79dad59242c7d8a951f Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 14 Jul 2025 13:55:59 -0700 Subject: [PATCH 05/40] fixing line endings --- src/Worker/Grpc/GrpcOrchestrationRunner.cs | 383 ++++++++++----------- 1 file changed, 191 insertions(+), 192 deletions(-) diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index 7c2fd0493..7285782bd 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using DurableTask.Core; -using DurableTask.Core.Exceptions; +using DurableTask.Core; using DurableTask.Core.History; using Google.Protobuf; -using Microsoft.DurableTask.Worker.Shims; +using Microsoft.DurableTask.Worker.Shims; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using P = Microsoft.DurableTask.Protobuf; @@ -25,106 +24,106 @@ namespace Microsoft.DurableTask.Worker.Grpc; /// /// public static class GrpcOrchestrationRunner -{ - /// - /// Loads orchestration history from and uses it to execute the - /// orchestrator function code pointed to by . - /// - /// - /// The type of the orchestrator function input. This type must be deserializable from JSON. - /// - /// - /// The type of the orchestrator function output. This type must be serializable to JSON. - /// - /// - /// The base64-encoded protobuf payload representing an orchestration execution request. - /// - /// A function that implements the orchestrator logic. - /// - /// Optional from which injected dependencies can be retrieved. - /// - /// - /// Returns a base64-encoded set of orchestrator actions to be interpreted by the external orchestration engine. - /// - /// - /// Thrown if or is null. - /// - public static string LoadAndRun( - string encodedOrchestratorRequest, - Func> orchestratorFunc, - IServiceProvider? services = null) - { - Check.NotNull(orchestratorFunc); - return LoadAndRun(encodedOrchestratorRequest, FuncTaskOrchestrator.Create(orchestratorFunc), services); - } - - /// - /// Deserializes orchestration history from and uses it to resume the - /// orchestrator implemented by . - /// - /// - /// The encoded protobuf payload representing an orchestration execution request. This is a base64-encoded string. - /// - /// - /// An implementation that defines the orchestrator logic. - /// - /// - /// Optional from which injected dependencies can be retrieved. - /// - /// - /// Returns a serialized set of orchestrator actions that should be used as the return value of the orchestrator function trigger. - /// - /// - /// Thrown if or is null. - /// - /// - /// Thrown if contains invalid data. - /// - public static string LoadAndRun( - string encodedOrchestratorRequest, - ITaskOrchestrator implementation, - IServiceProvider? services = null) - { - Check.NotNullOrEmpty(encodedOrchestratorRequest); - Check.NotNull(implementation); - - P.OrchestratorRequest request = P.OrchestratorRequest.Parser.Base64Decode( - encodedOrchestratorRequest); - - List pastEvents = request.PastEvents.Select(ProtoUtils.ConvertHistoryEvent).ToList(); - IEnumerable newEvents = request.NewEvents.Select(ProtoUtils.ConvertHistoryEvent); - Dictionary properties = request.Properties.ToDictionary( - pair => pair.Key, - pair => ProtoUtils.ConvertValueToObject(pair.Value)); - - // Re-construct the orchestration state from the history. - // New events must be added using the AddEvent method. - OrchestrationRuntimeState runtimeState = new(pastEvents); - foreach (HistoryEvent newEvent in newEvents) - { - runtimeState.AddEvent(newEvent); - } - - TaskName orchestratorName = new(runtimeState.Name); - ParentOrchestrationInstance? parent = runtimeState.ParentInstance is ParentInstance p - ? new(new(p.Name), p.OrchestrationInstance.InstanceId) - : null; - - DurableTaskShimFactory factory = services is null - ? DurableTaskShimFactory.Default - : ActivatorUtilities.GetServiceOrCreateInstance(services); - TaskOrchestration shim = factory.CreateOrchestration(orchestratorName, implementation, properties, parent); - TaskOrchestrationExecutor executor = new(runtimeState, shim, BehaviorOnContinueAsNew.Carryover, request.EntityParameters.ToCore(), ErrorPropagationMode.UseFailureDetails); - OrchestratorExecutionResult result = executor.Execute(); - - P.OrchestratorResponse response = ProtoUtils.ConstructOrchestratorResponse( - request.InstanceId, - result.CustomStatus, - result.Actions, - completionToken: string.Empty, /* doesn't apply */ - entityConversionState: null); - byte[] responseBytes = response.ToByteArray(); - return Convert.ToBase64String(responseBytes); +{ + /// + /// Loads orchestration history from and uses it to execute the + /// orchestrator function code pointed to by . + /// + /// + /// The type of the orchestrator function input. This type must be deserializable from JSON. + /// + /// + /// The type of the orchestrator function output. This type must be serializable to JSON. + /// + /// + /// The base64-encoded protobuf payload representing an orchestration execution request. + /// + /// A function that implements the orchestrator logic. + /// + /// Optional from which injected dependencies can be retrieved. + /// + /// + /// Returns a base64-encoded set of orchestrator actions to be interpreted by the external orchestration engine. + /// + /// + /// Thrown if or is null. + /// + public static string LoadAndRun( + string encodedOrchestratorRequest, + Func> orchestratorFunc, + IServiceProvider? services = null) + { + Check.NotNull(orchestratorFunc); + return LoadAndRun(encodedOrchestratorRequest, FuncTaskOrchestrator.Create(orchestratorFunc), services); + } + + /// + /// Deserializes orchestration history from and uses it to resume the + /// orchestrator implemented by . + /// + /// + /// The encoded protobuf payload representing an orchestration execution request. This is a base64-encoded string. + /// + /// + /// An implementation that defines the orchestrator logic. + /// + /// + /// Optional from which injected dependencies can be retrieved. + /// + /// + /// Returns a serialized set of orchestrator actions that should be used as the return value of the orchestrator function trigger. + /// + /// + /// Thrown if or is null. + /// + /// + /// Thrown if contains invalid data. + /// + public static string LoadAndRun( + string encodedOrchestratorRequest, + ITaskOrchestrator implementation, + IServiceProvider? services = null) + { + Check.NotNullOrEmpty(encodedOrchestratorRequest); + Check.NotNull(implementation); + + P.OrchestratorRequest request = P.OrchestratorRequest.Parser.Base64Decode( + encodedOrchestratorRequest); + + List pastEvents = request.PastEvents.Select(ProtoUtils.ConvertHistoryEvent).ToList(); + IEnumerable newEvents = request.NewEvents.Select(ProtoUtils.ConvertHistoryEvent); + Dictionary properties = request.Properties.ToDictionary( + pair => pair.Key, + pair => ProtoUtils.ConvertValueToObject(pair.Value)); + + // Re-construct the orchestration state from the history. + // New events must be added using the AddEvent method. + OrchestrationRuntimeState runtimeState = new(pastEvents); + foreach (HistoryEvent newEvent in newEvents) + { + runtimeState.AddEvent(newEvent); + } + + TaskName orchestratorName = new(runtimeState.Name); + ParentOrchestrationInstance? parent = runtimeState.ParentInstance is ParentInstance p + ? new(new(p.Name), p.OrchestrationInstance.InstanceId) + : null; + + DurableTaskShimFactory factory = services is null + ? DurableTaskShimFactory.Default + : ActivatorUtilities.GetServiceOrCreateInstance(services); + TaskOrchestration shim = factory.CreateOrchestration(orchestratorName, implementation, properties, parent); + TaskOrchestrationExecutor executor = new(runtimeState, shim, BehaviorOnContinueAsNew.Carryover, request.EntityParameters.ToCore(), ErrorPropagationMode.UseFailureDetails); + OrchestratorExecutionResult result = executor.Execute(); + + P.OrchestratorResponse response = ProtoUtils.ConstructOrchestratorResponse( + request.InstanceId, + result.CustomStatus, + result.Actions, + completionToken: string.Empty, /* doesn't apply */ + entityConversionState: null); + byte[] responseBytes = response.ToByteArray(); + return Convert.ToBase64String(responseBytes); } /// @@ -136,9 +135,9 @@ public static string LoadAndRun( /// /// /// An implementation that defines the orchestrator logic. - /// - /// - /// The cache of extended sessions which can be used to retrieve the if this orchestration request is from within an extended session. + /// + /// + /// The cache of extended sessions which can be used to retrieve the if this orchestration request is from within an extended session. /// /// /// Optional from which injected dependencies can be retrieved. @@ -154,7 +153,7 @@ public static string LoadAndRun( /// public static string LoadAndRun( string encodedOrchestratorRequest, - ITaskOrchestrator implementation, + ITaskOrchestrator implementation, IMemoryCache extendedSessions, IServiceProvider? services = null) { @@ -168,97 +167,97 @@ public static string LoadAndRun( IEnumerable newEvents = request.NewEvents.Select(ProtoUtils.ConvertHistoryEvent); Dictionary properties = request.Properties.ToDictionary( pair => pair.Key, - pair => ProtoUtils.ConvertValueToObject(pair.Value)); - - OrchestratorExecutionResult? result = null; - bool addToExtendedSessions = false; - bool requiresHistory = false; - if (properties.TryGetValue("ExtendedSession", out object? isExtendedSession) && (bool)isExtendedSession!) - { - if (extendedSessions.TryGetValue(request.InstanceId, out ExtendedSessionState? extendedSessionState)) - { - OrchestrationRuntimeState runtimeState = extendedSessionState!.RuntimeState; - runtimeState.NewEvents.Clear(); - foreach (HistoryEvent newEvent in newEvents) - { - runtimeState.AddEvent(newEvent); - } - - result = extendedSessionState.OrchestrationExecutor.ExecuteNewEvents(); - if (extendedSessionState.OrchestrationExecutor.IsCompleted) - { - extendedSessions.Remove(request.InstanceId); - } - } - else - { - addToExtendedSessions = true; - } - } - - if (result == null) - { - double extendedSessionIdleTimeoutInSeconds = 0; - if (properties.TryGetValue("ExtendedSessionIdleTimeoutInSeconds", out object? extendedSessionIdleTimeout) - && (double)extendedSessionIdleTimeout! >= 0) - { - extendedSessionIdleTimeoutInSeconds = (double)extendedSessionIdleTimeout!; - } - else - { - addToExtendedSessions = false; - } - - // If this is the first orchestration execution, then the past events count will be 0 but includePastEvents will be true (there are just none to include). - // Otherwise, there is an orchestration history but DurableTask.Core did not attach it since the extended session is still active on its end, but we have since evicted the - // session and lost the orchestration history so we cannot replay the orchestration. - if (pastEvents.Count == 0 && (properties.TryGetValue("IncludePastEvents", out object? pastEventsIncluded) && !(bool)pastEventsIncluded!)) - { - requiresHistory = true; - } - else - { - // Re-construct the orchestration state from the history. - // New events must be added using the AddEvent method. - OrchestrationRuntimeState runtimeState = new(pastEvents); - - foreach (HistoryEvent newEvent in newEvents) - { - runtimeState.AddEvent(newEvent); - } - - TaskName orchestratorName = new(runtimeState.Name); - ParentOrchestrationInstance? parent = runtimeState.ParentInstance is ParentInstance p - ? new(new(p.Name), p.OrchestrationInstance.InstanceId) - : null; - - DurableTaskShimFactory factory = services is null - ? DurableTaskShimFactory.Default - : ActivatorUtilities.GetServiceOrCreateInstance(services); - TaskOrchestration shim = factory.CreateOrchestration(orchestratorName, implementation, properties, parent); - TaskOrchestrationExecutor executor = new(runtimeState, shim, BehaviorOnContinueAsNew.Carryover, request.EntityParameters.ToCore(), ErrorPropagationMode.UseFailureDetails); - result = executor.Execute(); - - if (addToExtendedSessions && !executor.IsCompleted) - { - extendedSessions.Set( - request.InstanceId, - new(runtimeState, shim, executor), - new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(extendedSessionIdleTimeoutInSeconds) }); - } - else - { - extendedSessions.Remove(request.InstanceId); - } - } - } + pair => ProtoUtils.ConvertValueToObject(pair.Value)); + + OrchestratorExecutionResult? result = null; + bool addToExtendedSessions = false; + bool requiresHistory = false; + if (properties.TryGetValue("ExtendedSession", out object? isExtendedSession) && (bool)isExtendedSession!) + { + if (extendedSessions.TryGetValue(request.InstanceId, out ExtendedSessionState? extendedSessionState)) + { + OrchestrationRuntimeState runtimeState = extendedSessionState!.RuntimeState; + runtimeState.NewEvents.Clear(); + foreach (HistoryEvent newEvent in newEvents) + { + runtimeState.AddEvent(newEvent); + } + + result = extendedSessionState.OrchestrationExecutor.ExecuteNewEvents(); + if (extendedSessionState.OrchestrationExecutor.IsCompleted) + { + extendedSessions.Remove(request.InstanceId); + } + } + else + { + addToExtendedSessions = true; + } + } + + if (result == null) + { + double extendedSessionIdleTimeoutInSeconds = 0; + if (properties.TryGetValue("ExtendedSessionIdleTimeoutInSeconds", out object? extendedSessionIdleTimeout) + && (double)extendedSessionIdleTimeout! >= 0) + { + extendedSessionIdleTimeoutInSeconds = (double)extendedSessionIdleTimeout!; + } + else + { + addToExtendedSessions = false; + } + + // If this is the first orchestration execution, then the past events count will be 0 but includePastEvents will be true (there are just none to include). + // Otherwise, there is an orchestration history but DurableTask.Core did not attach it since the extended session is still active on its end, but we have since evicted the + // session and lost the orchestration history so we cannot replay the orchestration. + if (pastEvents.Count == 0 && (properties.TryGetValue("IncludePastEvents", out object? pastEventsIncluded) && !(bool)pastEventsIncluded!)) + { + requiresHistory = true; + } + else + { + // Re-construct the orchestration state from the history. + // New events must be added using the AddEvent method. + OrchestrationRuntimeState runtimeState = new(pastEvents); + + foreach (HistoryEvent newEvent in newEvents) + { + runtimeState.AddEvent(newEvent); + } + + TaskName orchestratorName = new(runtimeState.Name); + ParentOrchestrationInstance? parent = runtimeState.ParentInstance is ParentInstance p + ? new(new(p.Name), p.OrchestrationInstance.InstanceId) + : null; + + DurableTaskShimFactory factory = services is null + ? DurableTaskShimFactory.Default + : ActivatorUtilities.GetServiceOrCreateInstance(services); + TaskOrchestration shim = factory.CreateOrchestration(orchestratorName, implementation, properties, parent); + TaskOrchestrationExecutor executor = new(runtimeState, shim, BehaviorOnContinueAsNew.Carryover, request.EntityParameters.ToCore(), ErrorPropagationMode.UseFailureDetails); + result = executor.Execute(); + + if (addToExtendedSessions && !executor.IsCompleted) + { + extendedSessions.Set( + request.InstanceId, + new(runtimeState, shim, executor), + new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(extendedSessionIdleTimeoutInSeconds) }); + } + else + { + extendedSessions.Remove(request.InstanceId); + } + } + } P.OrchestratorResponse response = ProtoUtils.ConstructOrchestratorResponse( request.InstanceId, result?.CustomStatus, result?.Actions, completionToken: string.Empty, /* doesn't apply */ - entityConversionState: null, + entityConversionState: null, requiresHistory); byte[] responseBytes = response.ToByteArray(); return Convert.ToBase64String(responseBytes); From c23f1f649ebfb470ed0425ed81b0db5b6acd4bed Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 14 Jul 2025 14:06:25 -0700 Subject: [PATCH 06/40] added comments to the ExtendedSessionState class and the memory cache import to props --- Directory.Packages.props | 149 ++++++++++++------------ src/Worker/Grpc/ExtendedSessionState.cs | 31 +++-- 2 files changed, 98 insertions(+), 82 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 179d8fd74..0c018072a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,82 +1,83 @@ - - + + - true - - - - - - - - + --> + true + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/src/Worker/Grpc/ExtendedSessionState.cs b/src/Worker/Grpc/ExtendedSessionState.cs index 68b6e0729..13b5d05f7 100644 --- a/src/Worker/Grpc/ExtendedSessionState.cs +++ b/src/Worker/Grpc/ExtendedSessionState.cs @@ -6,20 +6,35 @@ namespace Microsoft.DurableTask.Worker.Grpc; /// -/// +/// Represents the state of an extended session for an orchestration. /// class ExtendedSessionState { + /// + /// Initializes a new instance of the class. + /// + /// The orchestration's runtime state. + /// The TaskOrchestration implementation of the orchestration. + /// The TaskOrchestrationExecutor for the orchestration. + internal ExtendedSessionState(OrchestrationRuntimeState state, TaskOrchestration taskOrchestration, TaskOrchestrationExecutor orchestrationExecutor) + { + this.RuntimeState = state; + this.TaskOrchestration = taskOrchestration; + this.OrchestrationExecutor = orchestrationExecutor; + } + + /// + /// Gets or sets the saved runtime state of the orchestration. + /// internal OrchestrationRuntimeState RuntimeState { get; set; } + /// + /// Gets or sets the saved TaskOrchestration implementation of the orchestration. + /// internal TaskOrchestration TaskOrchestration { get; set; } + /// + /// Gets or sets the saved TaskOrchestrationExecutor. + /// internal TaskOrchestrationExecutor OrchestrationExecutor { get; set; } - - public ExtendedSessionState(OrchestrationRuntimeState state, TaskOrchestration taskOrchestration, TaskOrchestrationExecutor orchestrationExecutor) - { - RuntimeState = state; - TaskOrchestration = taskOrchestration; - OrchestrationExecutor = orchestrationExecutor; - } } From d8b0332b1ed9fa17cc02fc5f723e70369bff86d1 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 14 Jul 2025 14:07:01 -0700 Subject: [PATCH 07/40] fixing props line endings --- Directory.Packages.props | 150 +++++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0c018072a..bf1a96565 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,83 +1,83 @@ - - + + - true - - - - - - - - - + --> + true + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + From 044f780d4234388daeab3741bbd5fb2f3a502379 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 1 Aug 2025 14:39:23 -0700 Subject: [PATCH 08/40] added a wrapper ExtendedSessionsCache object --- src/Worker/Grpc/ExtendedSessionsCache.cs | 21 ++++++++++++ src/Worker/Grpc/GrpcOrchestrationRunner.cs | 38 +++++++++++----------- 2 files changed, 40 insertions(+), 19 deletions(-) create mode 100644 src/Worker/Grpc/ExtendedSessionsCache.cs diff --git a/src/Worker/Grpc/ExtendedSessionsCache.cs b/src/Worker/Grpc/ExtendedSessionsCache.cs new file mode 100644 index 000000000..be6cc84e6 --- /dev/null +++ b/src/Worker/Grpc/ExtendedSessionsCache.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Caching.Memory; + +namespace Microsoft.DurableTask.Worker.Grpc; + +public class ExtendedSessionsCache +{ + private IMemoryCache? extendedSessions; + + internal IMemoryCache GetOrInitializeCache(double extendedSessionIdleTimeoutInSeconds) + { + this.extendedSessions ??= new MemoryCache(new MemoryCacheOptions + { + ExpirationScanFrequency = TimeSpan.FromSeconds(extendedSessionIdleTimeoutInSeconds), + }); + + return this.extendedSessions; + } +} diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index 7285782bd..72c9eb776 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -136,7 +136,7 @@ public static string LoadAndRun( /// /// An implementation that defines the orchestrator logic. /// - /// + /// /// The cache of extended sessions which can be used to retrieve the if this orchestration request is from within an extended session. /// /// @@ -154,7 +154,7 @@ public static string LoadAndRun( public static string LoadAndRun( string encodedOrchestratorRequest, ITaskOrchestrator implementation, - IMemoryCache extendedSessions, + ExtendedSessionsCache extendedSessionsCache, IServiceProvider? services = null) { Check.NotNullOrEmpty(encodedOrchestratorRequest); @@ -171,9 +171,20 @@ public static string LoadAndRun( OrchestratorExecutionResult? result = null; bool addToExtendedSessions = false; - bool requiresHistory = false; - if (properties.TryGetValue("ExtendedSession", out object? isExtendedSession) && (bool)isExtendedSession!) - { + bool requiresHistory = false; + double extendedSessionIdleTimeoutInSeconds = 0; + IMemoryCache? extendedSessions = null; + + if (properties.TryGetValue("ExtendedSession", out object? isExtendedSessionObj) + && isExtendedSessionObj is bool isExtendedSession + && isExtendedSession + && properties.TryGetValue("ExtendedSessionIdleTimeoutInSeconds", out object? extendedSessionIdleTimeoutObj) + && extendedSessionIdleTimeoutObj is double extendedSessionIdleTimeout + && extendedSessionIdleTimeout >= 0) + { + extendedSessionIdleTimeoutInSeconds = extendedSessionIdleTimeout; + extendedSessions = extendedSessionsCache.GetOrInitializeCache(extendedSessionIdleTimeoutInSeconds); + if (extendedSessions.TryGetValue(request.InstanceId, out ExtendedSessionState? extendedSessionState)) { OrchestrationRuntimeState runtimeState = extendedSessionState!.RuntimeState; @@ -192,22 +203,11 @@ public static string LoadAndRun( else { addToExtendedSessions = true; - } + } } if (result == null) { - double extendedSessionIdleTimeoutInSeconds = 0; - if (properties.TryGetValue("ExtendedSessionIdleTimeoutInSeconds", out object? extendedSessionIdleTimeout) - && (double)extendedSessionIdleTimeout! >= 0) - { - extendedSessionIdleTimeoutInSeconds = (double)extendedSessionIdleTimeout!; - } - else - { - addToExtendedSessions = false; - } - // If this is the first orchestration execution, then the past events count will be 0 but includePastEvents will be true (there are just none to include). // Otherwise, there is an orchestration history but DurableTask.Core did not attach it since the extended session is still active on its end, but we have since evicted the // session and lost the orchestration history so we cannot replay the orchestration. @@ -240,14 +240,14 @@ public static string LoadAndRun( if (addToExtendedSessions && !executor.IsCompleted) { - extendedSessions.Set( + extendedSessions!.Set( request.InstanceId, new(runtimeState, shim, executor), new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(extendedSessionIdleTimeoutInSeconds) }); } else { - extendedSessions.Remove(request.InstanceId); + extendedSessions?.Remove(request.InstanceId); } } } From b5d84812f3f1439416d43434c966b103a5bfe15c Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 1 Aug 2025 14:47:39 -0700 Subject: [PATCH 09/40] added comments --- src/Worker/Grpc/ExtendedSessionsCache.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Worker/Grpc/ExtendedSessionsCache.cs b/src/Worker/Grpc/ExtendedSessionsCache.cs index be6cc84e6..c54fe4cb3 100644 --- a/src/Worker/Grpc/ExtendedSessionsCache.cs +++ b/src/Worker/Grpc/ExtendedSessionsCache.cs @@ -5,15 +5,27 @@ namespace Microsoft.DurableTask.Worker.Grpc; +/// +/// A cache for extended sessions that wraps an instance. +/// Responsible for holding for orchestrations that are running within extended sessions. +/// public class ExtendedSessionsCache { - private IMemoryCache? extendedSessions; + IMemoryCache? extendedSessions; - internal IMemoryCache GetOrInitializeCache(double extendedSessionIdleTimeoutInSeconds) + /// + /// Gets the cache for extended sessions if it has already been initialized, or otherwise initializes it with the given expiration scan frequency. + /// + /// + /// The expiration scan frequency of the cache, in seconds. T + /// This specifies how often the cache checks for stale items, and evicts them. + /// + /// The IMemoryCache that holds the cached . + internal IMemoryCache GetOrInitializeCache(double expirationScanFrequencyInSeconds) { this.extendedSessions ??= new MemoryCache(new MemoryCacheOptions { - ExpirationScanFrequency = TimeSpan.FromSeconds(extendedSessionIdleTimeoutInSeconds), + ExpirationScanFrequency = TimeSpan.FromSeconds(expirationScanFrequencyInSeconds), }); return this.extendedSessions; From 0def00360299e5671cda5b81b4360e3f7453a9b5 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 1 Aug 2025 14:49:31 -0700 Subject: [PATCH 10/40] updated the expiration scan frequency --- src/Worker/Grpc/ExtendedSessionsCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Worker/Grpc/ExtendedSessionsCache.cs b/src/Worker/Grpc/ExtendedSessionsCache.cs index c54fe4cb3..89c8f643d 100644 --- a/src/Worker/Grpc/ExtendedSessionsCache.cs +++ b/src/Worker/Grpc/ExtendedSessionsCache.cs @@ -25,7 +25,7 @@ internal IMemoryCache GetOrInitializeCache(double expirationScanFrequencyInSecon { this.extendedSessions ??= new MemoryCache(new MemoryCacheOptions { - ExpirationScanFrequency = TimeSpan.FromSeconds(expirationScanFrequencyInSeconds), + ExpirationScanFrequency = TimeSpan.FromSeconds(expirationScanFrequencyInSeconds / 5), }); return this.extendedSessions; From 306dd397112cbaee8069428175c00f892c26a900 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 1 Aug 2025 14:53:36 -0700 Subject: [PATCH 11/40] addressing some comments --- src/Shared/Grpc/ProtoUtils.cs | 236 +++++++++++---------- src/Worker/Grpc/GrpcOrchestrationRunner.cs | 2 +- src/Worker/Grpc/Worker.Grpc.csproj | 4 + 3 files changed, 124 insertions(+), 118 deletions(-) diff --git a/src/Shared/Grpc/ProtoUtils.cs b/src/Shared/Grpc/ProtoUtils.cs index 8c47c9105..5a60b7131 100644 --- a/src/Shared/Grpc/ProtoUtils.cs +++ b/src/Shared/Grpc/ProtoUtils.cs @@ -295,147 +295,149 @@ internal static P.OrchestratorResponse ConstructOrchestratorResponse( }; // If a history is required and the orchestration request was not completed, then there is no list of actions. - if (!requiresHistory) + if (requiresHistory) { - Check.NotNull(actions); - foreach (OrchestratorAction action in actions) - { - var protoAction = new P.OrchestratorAction { Id = action.Id }; + return response; + } - switch (action.OrchestratorActionType) - { - case OrchestratorActionType.ScheduleOrchestrator: - var scheduleTaskAction = (ScheduleTaskOrchestratorAction)action; - protoAction.ScheduleTask = new P.ScheduleTaskAction - { - Name = scheduleTaskAction.Name, - Version = scheduleTaskAction.Version, - Input = scheduleTaskAction.Input, - }; + Check.NotNull(actions); + foreach (OrchestratorAction action in actions) + { + var protoAction = new P.OrchestratorAction { Id = action.Id }; - if (scheduleTaskAction.Tags != null) - { - foreach (KeyValuePair tag in scheduleTaskAction.Tags) - { - protoAction.ScheduleTask.Tags[tag.Key] = tag.Value; - } - } + switch (action.OrchestratorActionType) + { + case OrchestratorActionType.ScheduleOrchestrator: + var scheduleTaskAction = (ScheduleTaskOrchestratorAction)action; + protoAction.ScheduleTask = new P.ScheduleTaskAction + { + Name = scheduleTaskAction.Name, + Version = scheduleTaskAction.Version, + Input = scheduleTaskAction.Input, + }; - break; - case OrchestratorActionType.CreateSubOrchestration: - var subOrchestrationAction = (CreateSubOrchestrationAction)action; - protoAction.CreateSubOrchestration = new P.CreateSubOrchestrationAction - { - Input = subOrchestrationAction.Input, - InstanceId = subOrchestrationAction.InstanceId, - Name = subOrchestrationAction.Name, - Version = subOrchestrationAction.Version, - }; - break; - case OrchestratorActionType.CreateTimer: - var createTimerAction = (CreateTimerOrchestratorAction)action; - protoAction.CreateTimer = new P.CreateTimerAction - { - FireAt = createTimerAction.FireAt.ToTimestamp(), - }; - break; - case OrchestratorActionType.SendEvent: - var sendEventAction = (SendEventOrchestratorAction)action; - if (sendEventAction.Instance == null) + if (scheduleTaskAction.Tags != null) + { + foreach (KeyValuePair tag in scheduleTaskAction.Tags) { - throw new ArgumentException( - $"{nameof(SendEventOrchestratorAction)} cannot have a null Instance property!"); + protoAction.ScheduleTask.Tags[tag.Key] = tag.Value; } + } - if (entityConversionState is not null - && DTCore.Common.Entities.IsEntityInstance(sendEventAction.Instance.InstanceId) - && sendEventAction.EventName is not null - && sendEventAction.EventData is not null) - { - P.SendEntityMessageAction sendAction = new P.SendEntityMessageAction(); - protoAction.SendEntityMessage = sendAction; + break; + case OrchestratorActionType.CreateSubOrchestration: + var subOrchestrationAction = (CreateSubOrchestrationAction)action; + protoAction.CreateSubOrchestration = new P.CreateSubOrchestrationAction + { + Input = subOrchestrationAction.Input, + InstanceId = subOrchestrationAction.InstanceId, + Name = subOrchestrationAction.Name, + Version = subOrchestrationAction.Version, + }; + break; + case OrchestratorActionType.CreateTimer: + var createTimerAction = (CreateTimerOrchestratorAction)action; + protoAction.CreateTimer = new P.CreateTimerAction + { + FireAt = createTimerAction.FireAt.ToTimestamp(), + }; + break; + case OrchestratorActionType.SendEvent: + var sendEventAction = (SendEventOrchestratorAction)action; + if (sendEventAction.Instance == null) + { + throw new ArgumentException( + $"{nameof(SendEventOrchestratorAction)} cannot have a null Instance property!"); + } + + if (entityConversionState is not null + && DTCore.Common.Entities.IsEntityInstance(sendEventAction.Instance.InstanceId) + && sendEventAction.EventName is not null + && sendEventAction.EventData is not null) + { + P.SendEntityMessageAction sendAction = new P.SendEntityMessageAction(); + protoAction.SendEntityMessage = sendAction; - EntityConversions.DecodeEntityMessageAction( - sendEventAction.EventName, - sendEventAction.EventData, - sendEventAction.Instance.InstanceId, - sendAction, - out string requestId); + EntityConversions.DecodeEntityMessageAction( + sendEventAction.EventName, + sendEventAction.EventData, + sendEventAction.Instance.InstanceId, + sendAction, + out string requestId); - entityConversionState.EntityRequestIds.Add(requestId); + entityConversionState.EntityRequestIds.Add(requestId); - switch (sendAction.EntityMessageTypeCase) - { - case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityLockRequested: - entityConversionState.AddUnlockObligations(sendAction.EntityLockRequested); - break; - case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityUnlockSent: - entityConversionState.RemoveUnlockObligation(sendAction.EntityUnlockSent.TargetInstanceId); - break; - default: - break; - } - } - else + switch (sendAction.EntityMessageTypeCase) { - protoAction.SendEvent = new P.SendEventAction - { - Instance = sendEventAction.Instance.ToProtobuf(), - Name = sendEventAction.EventName, - Data = sendEventAction.EventData, - }; + case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityLockRequested: + entityConversionState.AddUnlockObligations(sendAction.EntityLockRequested); + break; + case P.SendEntityMessageAction.EntityMessageTypeOneofCase.EntityUnlockSent: + entityConversionState.RemoveUnlockObligation(sendAction.EntityUnlockSent.TargetInstanceId); + break; + default: + break; } + } + else + { + protoAction.SendEvent = new P.SendEventAction + { + Instance = sendEventAction.Instance.ToProtobuf(), + Name = sendEventAction.EventName, + Data = sendEventAction.EventData, + }; + } - break; - case OrchestratorActionType.OrchestrationComplete: + break; + case OrchestratorActionType.OrchestrationComplete: - if (entityConversionState is not null) + if (entityConversionState is not null) + { + // as a precaution, unlock any entities that were not unlocked for some reason, before + // completing the orchestration. + foreach ((string target, string criticalSectionId) in entityConversionState.ResetObligations()) { - // as a precaution, unlock any entities that were not unlocked for some reason, before - // completing the orchestration. - foreach ((string target, string criticalSectionId) in entityConversionState.ResetObligations()) + response.Actions.Add(new P.OrchestratorAction { - response.Actions.Add(new P.OrchestratorAction + Id = action.Id, + SendEntityMessage = new P.SendEntityMessageAction { - Id = action.Id, - SendEntityMessage = new P.SendEntityMessageAction + EntityUnlockSent = new P.EntityUnlockSentEvent { - EntityUnlockSent = new P.EntityUnlockSentEvent - { - CriticalSectionId = criticalSectionId, - TargetInstanceId = target, - ParentInstanceId = entityConversionState.CurrentInstance?.InstanceId, - }, + CriticalSectionId = criticalSectionId, + TargetInstanceId = target, + ParentInstanceId = entityConversionState.CurrentInstance?.InstanceId, }, - }); - } + }, + }); } + } - var completeAction = (OrchestrationCompleteOrchestratorAction)action; - protoAction.CompleteOrchestration = new P.CompleteOrchestrationAction - { - CarryoverEvents = - { - // TODO - }, - Details = completeAction.Details, - NewVersion = completeAction.NewVersion, - OrchestrationStatus = completeAction.OrchestrationStatus.ToProtobuf(), - Result = completeAction.Result, - }; - - if (completeAction.OrchestrationStatus == OrchestrationStatus.Failed) - { - protoAction.CompleteOrchestration.FailureDetails = completeAction.FailureDetails.ToProtobuf(); - } + var completeAction = (OrchestrationCompleteOrchestratorAction)action; + protoAction.CompleteOrchestration = new P.CompleteOrchestrationAction + { + CarryoverEvents = + { + // TODO + }, + Details = completeAction.Details, + NewVersion = completeAction.NewVersion, + OrchestrationStatus = completeAction.OrchestrationStatus.ToProtobuf(), + Result = completeAction.Result, + }; - break; - default: - throw new NotSupportedException($"Unknown orchestrator action: {action.OrchestratorActionType}"); - } + if (completeAction.OrchestrationStatus == OrchestrationStatus.Failed) + { + protoAction.CompleteOrchestration.FailureDetails = completeAction.FailureDetails.ToProtobuf(); + } - response.Actions.Add(protoAction); + break; + default: + throw new NotSupportedException($"Unknown orchestrator action: {action.OrchestratorActionType}"); } + + response.Actions.Add(protoAction); } return response; diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index 72c9eb776..7f8cb423b 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -185,7 +185,7 @@ public static string LoadAndRun( extendedSessionIdleTimeoutInSeconds = extendedSessionIdleTimeout; extendedSessions = extendedSessionsCache.GetOrInitializeCache(extendedSessionIdleTimeoutInSeconds); - if (extendedSessions.TryGetValue(request.InstanceId, out ExtendedSessionState? extendedSessionState)) + if (extendedSessions.TryGetValue(request.InstanceId, out ExtendedSessionState? extendedSessionState) && extendedSessionState is not null) { OrchestrationRuntimeState runtimeState = extendedSessionState!.RuntimeState; runtimeState.NewEvents.Clear(); diff --git a/src/Worker/Grpc/Worker.Grpc.csproj b/src/Worker/Grpc/Worker.Grpc.csproj index 991fef8f1..5d998be06 100644 --- a/src/Worker/Grpc/Worker.Grpc.csproj +++ b/src/Worker/Grpc/Worker.Grpc.csproj @@ -6,6 +6,10 @@ true + + + + From 834711b55a34a05bcaabdb803bb1aafa1addec72 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 1 Aug 2025 14:54:53 -0700 Subject: [PATCH 12/40] fixing indentation --- src/Shared/Grpc/ProtoUtils.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Shared/Grpc/ProtoUtils.cs b/src/Shared/Grpc/ProtoUtils.cs index 5a60b7131..5cbcbd047 100644 --- a/src/Shared/Grpc/ProtoUtils.cs +++ b/src/Shared/Grpc/ProtoUtils.cs @@ -418,9 +418,9 @@ internal static P.OrchestratorResponse ConstructOrchestratorResponse( protoAction.CompleteOrchestration = new P.CompleteOrchestrationAction { CarryoverEvents = - { - // TODO - }, + { + // TODO + }, Details = completeAction.Details, NewVersion = completeAction.NewVersion, OrchestrationStatus = completeAction.OrchestrationStatus.ToProtobuf(), From 3f24e5fe0c87087d10a340f0296bbc757d14b599 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 1 Aug 2025 15:36:56 -0700 Subject: [PATCH 13/40] adding proto files --- src/Grpc/orchestrator_service.proto | 1 + src/Grpc/versions.txt | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Grpc/orchestrator_service.proto b/src/Grpc/orchestrator_service.proto index b2ca147b5..6d99eee5e 100644 --- a/src/Grpc/orchestrator_service.proto +++ b/src/Grpc/orchestrator_service.proto @@ -333,6 +333,7 @@ message OrchestratorResponse { // The number of work item events that were processed by the orchestrator. // This field is optional. If not set, the service should assume that the orchestrator processed all events. google.protobuf.Int32Value numEventsProcessed = 5; + bool requiresHistory = 6; } message CreateInstanceRequest { diff --git a/src/Grpc/versions.txt b/src/Grpc/versions.txt index 121ec0ec7..65d908180 100644 --- a/src/Grpc/versions.txt +++ b/src/Grpc/versions.txt @@ -1,2 +1,2 @@ -# The following files were downloaded from branch main at 2025-06-02 21:12:34 UTC -https://raw.githubusercontent.com/microsoft/durabletask-protobuf/fd9369c6a03d6af4e95285e432b7c4e943c06970/protos/orchestrator_service.proto +# The following files were downloaded from branch stevosyan/extended-sessions-for-orchestrations-isolated at 2025-08-01 19:05:01 UTC +https://raw.githubusercontent.com/microsoft/durabletask-protobuf/7d162b42da7a2cffc0927a3f85c3c8b373f136c7/protos/orchestrator_service.proto From 206457dced69c007d257afe0f41d35df1327c682 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 1 Aug 2025 16:10:28 -0700 Subject: [PATCH 14/40] added a max frequency to cache scan expiration --- src/Worker/Grpc/ExtendedSessionsCache.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Worker/Grpc/ExtendedSessionsCache.cs b/src/Worker/Grpc/ExtendedSessionsCache.cs index 89c8f643d..741eac0b9 100644 --- a/src/Worker/Grpc/ExtendedSessionsCache.cs +++ b/src/Worker/Grpc/ExtendedSessionsCache.cs @@ -25,7 +25,8 @@ internal IMemoryCache GetOrInitializeCache(double expirationScanFrequencyInSecon { this.extendedSessions ??= new MemoryCache(new MemoryCacheOptions { - ExpirationScanFrequency = TimeSpan.FromSeconds(expirationScanFrequencyInSeconds / 5), + // To avoid overloading the system with too-frequent scans, with cap the scanning frequency at 3 seconds. + ExpirationScanFrequency = TimeSpan.FromSeconds(Math.Max(expirationScanFrequencyInSeconds / 5, 3)), }); return this.extendedSessions; From 754948ea1cac706eae3286f977552b31f7cd986f Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 1 Aug 2025 16:40:22 -0700 Subject: [PATCH 15/40] reverting to old implementation without a max --- src/Worker/Grpc/ExtendedSessionsCache.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Worker/Grpc/ExtendedSessionsCache.cs b/src/Worker/Grpc/ExtendedSessionsCache.cs index 741eac0b9..89c8f643d 100644 --- a/src/Worker/Grpc/ExtendedSessionsCache.cs +++ b/src/Worker/Grpc/ExtendedSessionsCache.cs @@ -25,8 +25,7 @@ internal IMemoryCache GetOrInitializeCache(double expirationScanFrequencyInSecon { this.extendedSessions ??= new MemoryCache(new MemoryCacheOptions { - // To avoid overloading the system with too-frequent scans, with cap the scanning frequency at 3 seconds. - ExpirationScanFrequency = TimeSpan.FromSeconds(Math.Max(expirationScanFrequencyInSeconds / 5, 3)), + ExpirationScanFrequency = TimeSpan.FromSeconds(expirationScanFrequencyInSeconds / 5), }); return this.extendedSessions; From 80165ead859088491e3b7ee049ea9703dcdfc84e Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 1 Aug 2025 16:47:04 -0700 Subject: [PATCH 16/40] addressing comments --- src/Worker/Grpc/ExtendedSessionsCache.cs | 19 ++++++++++++++----- src/Worker/Grpc/GrpcOrchestrationRunner.cs | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Worker/Grpc/ExtendedSessionsCache.cs b/src/Worker/Grpc/ExtendedSessionsCache.cs index 89c8f643d..39b630618 100644 --- a/src/Worker/Grpc/ExtendedSessionsCache.cs +++ b/src/Worker/Grpc/ExtendedSessionsCache.cs @@ -6,22 +6,31 @@ namespace Microsoft.DurableTask.Worker.Grpc; /// -/// A cache for extended sessions that wraps an instance. +/// A cache for extended sessions that wraps a instance. /// Responsible for holding for orchestrations that are running within extended sessions. /// -public class ExtendedSessionsCache +public class ExtendedSessionsCache : IDisposable { - IMemoryCache? extendedSessions; + MemoryCache? extendedSessions; + + /// + /// Dispose the cache and release all resources. + /// + public void Dispose() + { + this.extendedSessions?.Dispose(); + GC.SuppressFinalize(this); + } /// /// Gets the cache for extended sessions if it has already been initialized, or otherwise initializes it with the given expiration scan frequency. /// /// - /// The expiration scan frequency of the cache, in seconds. T + /// The expiration scan frequency of the cache, in seconds. /// This specifies how often the cache checks for stale items, and evicts them. /// /// The IMemoryCache that holds the cached . - internal IMemoryCache GetOrInitializeCache(double expirationScanFrequencyInSeconds) + internal MemoryCache GetOrInitializeCache(double expirationScanFrequencyInSeconds) { this.extendedSessions ??= new MemoryCache(new MemoryCacheOptions { diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index 7f8cb423b..17ac8b62f 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -173,7 +173,7 @@ public static string LoadAndRun( bool addToExtendedSessions = false; bool requiresHistory = false; double extendedSessionIdleTimeoutInSeconds = 0; - IMemoryCache? extendedSessions = null; + MemoryCache? extendedSessions = null; if (properties.TryGetValue("ExtendedSession", out object? isExtendedSessionObj) && isExtendedSessionObj is bool isExtendedSession From eac9190b399a3ba3a841513a68175d0e0112b6bf Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 1 Aug 2025 16:50:59 -0700 Subject: [PATCH 17/40] adding updated protos --- src/Grpc/orchestrator_service.proto | 1 + src/Grpc/versions.txt | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Grpc/orchestrator_service.proto b/src/Grpc/orchestrator_service.proto index 6d99eee5e..8b22a850a 100644 --- a/src/Grpc/orchestrator_service.proto +++ b/src/Grpc/orchestrator_service.proto @@ -333,6 +333,7 @@ message OrchestratorResponse { // The number of work item events that were processed by the orchestrator. // This field is optional. If not set, the service should assume that the orchestrator processed all events. google.protobuf.Int32Value numEventsProcessed = 5; + // Whether or not a history is required to complete the original OrchestratorRequest and none was provided. bool requiresHistory = 6; } diff --git a/src/Grpc/versions.txt b/src/Grpc/versions.txt index 65d908180..aa9634683 100644 --- a/src/Grpc/versions.txt +++ b/src/Grpc/versions.txt @@ -1,2 +1,2 @@ -# The following files were downloaded from branch stevosyan/extended-sessions-for-orchestrations-isolated at 2025-08-01 19:05:01 UTC -https://raw.githubusercontent.com/microsoft/durabletask-protobuf/7d162b42da7a2cffc0927a3f85c3c8b373f136c7/protos/orchestrator_service.proto +# The following files were downloaded from branch stevosyan/extended-sessions-for-orchestrations-isolated at 2025-08-01 23:50:40 UTC +https://raw.githubusercontent.com/microsoft/durabletask-protobuf/930a06ef7df4f3156fdaa619e8e4d6439d43fcb5/protos/orchestrator_service.proto From 298d9116313d654b7980c04c7f8cea59fd7e5b84 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 1 Aug 2025 17:03:40 -0700 Subject: [PATCH 18/40] added null check --- src/Worker/Grpc/GrpcOrchestrationRunner.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index 17ac8b62f..373c66223 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -158,7 +158,8 @@ public static string LoadAndRun( IServiceProvider? services = null) { Check.NotNullOrEmpty(encodedOrchestratorRequest); - Check.NotNull(implementation); + Check.NotNull(implementation); + Check.NotNull(extendedSessionsCache); P.OrchestratorRequest request = P.OrchestratorRequest.Parser.Base64Decode( encodedOrchestratorRequest); From 2c218719bd3cfdb90f2148152a13f4051d8fd231 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 1 Aug 2025 17:08:57 -0700 Subject: [PATCH 19/40] missed an earlier PR comment --- src/Worker/Grpc/GrpcOrchestrationRunner.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index 373c66223..389e06ab7 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -212,7 +212,10 @@ public static string LoadAndRun( // If this is the first orchestration execution, then the past events count will be 0 but includePastEvents will be true (there are just none to include). // Otherwise, there is an orchestration history but DurableTask.Core did not attach it since the extended session is still active on its end, but we have since evicted the // session and lost the orchestration history so we cannot replay the orchestration. - if (pastEvents.Count == 0 && (properties.TryGetValue("IncludePastEvents", out object? pastEventsIncluded) && !(bool)pastEventsIncluded!)) + if (pastEvents.Count == 0 + && (properties.TryGetValue("IncludePastEvents", out object? pastEventsIncludedObj) + && pastEventsIncludedObj is bool pastEventsIncluded + && !pastEventsIncluded)) { requiresHistory = true; } From e6860a9e7d96fb34f2b5e0b4901526d4478d0aa8 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 4 Aug 2025 11:28:46 -0700 Subject: [PATCH 20/40] added the durabletask packages from the test source --- Directory.Packages.props | 2 +- eng/targets/Release.props | 4 ++-- nuget.config | 21 +++++++++++++++------ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index bbc9c239b..5b0307aaa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,7 +29,7 @@ - + diff --git a/eng/targets/Release.props b/eng/targets/Release.props index bf40e2a27..cbfc2de3d 100644 --- a/eng/targets/Release.props +++ b/eng/targets/Release.props @@ -17,8 +17,8 @@ - 1.12.0 - + 1.12.1 + private1 diff --git a/nuget.config b/nuget.config index 6f5093913..719044ea2 100644 --- a/nuget.config +++ b/nuget.config @@ -1,7 +1,16 @@ - - - - - - + + + + + + + + + + + + + + + \ No newline at end of file From 77794ac2be97a65136f8236e9d5c79da2b066176 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 4 Aug 2025 15:48:27 -0700 Subject: [PATCH 21/40] updated reference to new preview package --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5b0307aaa..d9e70ac48 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,7 +29,7 @@ - + From 8e1c6c1a9bf406765d80fb3fc217d690f4e47af9 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 4 Aug 2025 16:01:54 -0700 Subject: [PATCH 22/40] pushing the updated yml --- eng/templates/build.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/eng/templates/build.yml b/eng/templates/build.yml index a94d3aff0..713fc63ff 100644 --- a/eng/templates/build.yml +++ b/eng/templates/build.yml @@ -41,11 +41,10 @@ jobs: - task: DotNetCoreCLI@2 displayName: Restore inputs: - command: restore - verbosityRestore: Minimal + command: custom + custom: restore projects: $(project) - feedsToUse: config - nugetConfigPath: nuget.config + arguments: '-v m' # Build source directory - task: DotNetCoreCLI@2 From a5252de24a35ccd939863e45937df4fcad6eba1f Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 4 Aug 2025 16:12:06 -0700 Subject: [PATCH 23/40] forgot to update the release package name --- eng/targets/Release.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/targets/Release.props b/eng/targets/Release.props index cbfc2de3d..860292f49 100644 --- a/eng/targets/Release.props +++ b/eng/targets/Release.props @@ -18,7 +18,7 @@ 1.12.1 - private1 + private2 From 3c40541b60ce0ad04adfce520544e57d62308cad Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Wed, 6 Aug 2025 15:23:51 -0700 Subject: [PATCH 24/40] adding unit tests --- src/Worker/Grpc/ExtendedSessionsCache.cs | 11 +- src/Worker/Grpc/GrpcOrchestrationRunner.cs | 13 +- .../GrpcOrchestrationRunnerTests.cs | 383 ++++++++++++++++++ test/Worker/Core.Tests/Worker.Tests.csproj | 1 + 4 files changed, 402 insertions(+), 6 deletions(-) create mode 100644 test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs diff --git a/src/Worker/Grpc/ExtendedSessionsCache.cs b/src/Worker/Grpc/ExtendedSessionsCache.cs index 39b630618..90254c1c5 100644 --- a/src/Worker/Grpc/ExtendedSessionsCache.cs +++ b/src/Worker/Grpc/ExtendedSessionsCache.cs @@ -30,7 +30,7 @@ public void Dispose() /// This specifies how often the cache checks for stale items, and evicts them. /// /// The IMemoryCache that holds the cached . - internal MemoryCache GetOrInitializeCache(double expirationScanFrequencyInSeconds) + public MemoryCache GetOrInitializeCache(double expirationScanFrequencyInSeconds) { this.extendedSessions ??= new MemoryCache(new MemoryCacheOptions { @@ -39,4 +39,13 @@ internal MemoryCache GetOrInitializeCache(double expirationScanFrequencyInSecond return this.extendedSessions; } + + /// + /// Returns whether or not the cache has been initialized. + /// + /// True if the cache has been initialized, false otherwise. + public bool IsInitialized() + { + return this.extendedSessions is not null; + } } diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index 389e06ab7..8fa3d6312 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -175,17 +175,20 @@ public static string LoadAndRun( bool requiresHistory = false; double extendedSessionIdleTimeoutInSeconds = 0; MemoryCache? extendedSessions = null; - - if (properties.TryGetValue("ExtendedSession", out object? isExtendedSessionObj) - && isExtendedSessionObj is bool isExtendedSession - && isExtendedSession - && properties.TryGetValue("ExtendedSessionIdleTimeoutInSeconds", out object? extendedSessionIdleTimeoutObj) + + if (properties.TryGetValue("ExtendedSessionIdleTimeoutInSeconds", out object? extendedSessionIdleTimeoutObj) && extendedSessionIdleTimeoutObj is double extendedSessionIdleTimeout && extendedSessionIdleTimeout >= 0) { extendedSessionIdleTimeoutInSeconds = extendedSessionIdleTimeout; extendedSessions = extendedSessionsCache.GetOrInitializeCache(extendedSessionIdleTimeoutInSeconds); + } + if (properties.TryGetValue("ExtendedSession", out object? isExtendedSessionObj) + && isExtendedSessionObj is bool isExtendedSession + && isExtendedSession + && extendedSessions != null) + { if (extendedSessions.TryGetValue(request.InstanceId, out ExtendedSessionState? extendedSessionState) && extendedSessionState is not null) { OrchestrationRuntimeState runtimeState = extendedSessionState!.RuntimeState; diff --git a/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs b/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs new file mode 100644 index 000000000..7dbfb6d35 --- /dev/null +++ b/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs @@ -0,0 +1,383 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Google.Protobuf; +using Google.Protobuf.Collections; +using Google.Protobuf.WellKnownTypes; +using Microsoft.DurableTask.Worker.Grpc; + +namespace Microsoft.DurableTask.Worker.Tests; +public class GrpcOrchestrationRunnerTests +{ + const string TestInstanceId = "instance_id"; + const string TestExecutionId = "execution_id"; + const int DefaultExtendedSessionIdleTimeoutInSeconds = 300; + + [Fact] + public void EmptyOrNullParameters_Throw() + { + Action act = () => + GrpcOrchestrationRunner.LoadAndRun(string.Empty, new SimpleOrchestrator(), new ExtendedSessionsCache()); + act.Should().ThrowExactly().WithParameterName("encodedOrchestratorRequest"); + + act = () => + GrpcOrchestrationRunner.LoadAndRun(null!, new SimpleOrchestrator(), new ExtendedSessionsCache()); + act.Should().ThrowExactly().WithParameterName("encodedOrchestratorRequest"); + + act = () => + GrpcOrchestrationRunner.LoadAndRun("request", null!, new ExtendedSessionsCache()); + act.Should().ThrowExactly().WithParameterName("implementation"); + + act = () => + GrpcOrchestrationRunner.LoadAndRun("request", new SimpleOrchestrator(), extendedSessionsCache: null!); + act.Should().ThrowExactly().WithParameterName("extendedSessionsCache"); + } + + [Fact] + public void EmptyHistory_Returns_NeedsHistoryInResponse() + { + using var extendedSessions = new ExtendedSessionsCache(); + + // No history and without extended sessions enabled + var orchestratorRequest = CreateOrchestratorRequest([]); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(false) }, + { "ExtendedSession", Value.ForBool(false) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + byte[] requestBytes = orchestratorRequest.ToByteArray(); + string requestString = Convert.ToBase64String(requestBytes); + string stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + Protobuf.OrchestratorResponse response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); + Assert.True(response.RequiresHistory); + Assert.False(extendedSessions.IsInitialized()); + + // No history but with extended sessions enabled + orchestratorRequest.Properties.Clear(); + orchestratorRequest.Properties.Add(new MapField() { + { "ExtendedSession", Value.ForBool(true) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + requestBytes = orchestratorRequest.ToByteArray(); + requestString = Convert.ToBase64String(requestBytes); + stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); + Assert.True(response.RequiresHistory); + Assert.True(extendedSessions.IsInitialized()); + } + + [Fact] + public void MalformedRequestParameters_Means_NoExtendedSessionsStored() + { + using var extendedSessions = new ExtendedSessionsCache(); + var orchestratorRequest = CreateOrchestratorRequest([]); + + // Misspelled extended session timeout key + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(false) }, + { "ExtendedSession", Value.ForBool(true) }, + { "ExtendedSessionsIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + byte[] requestBytes = orchestratorRequest.ToByteArray(); + string requestString = Convert.ToBase64String(requestBytes); + string stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + Protobuf.OrchestratorResponse response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); + Assert.False(extendedSessions.IsInitialized()); + + // Wrong value type for extended session timeout key + orchestratorRequest.Properties.Clear(); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(false) }, + { "ExtendedSession", Value.ForBool(true) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForString("hi") } }); + requestBytes = orchestratorRequest.ToByteArray(); + requestString = Convert.ToBase64String(requestBytes); + GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + Assert.False(extendedSessions.IsInitialized()); + + // No extended session timeout key + orchestratorRequest.Properties.Clear(); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(false) }, + { "ExtendedSession", Value.ForBool(true) } }); + requestBytes = orchestratorRequest.ToByteArray(); + requestString = Convert.ToBase64String(requestBytes); + GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + Assert.False(extendedSessions.IsInitialized()); + + // Misspelled extended session key + orchestratorRequest.Properties.Clear(); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(false) }, + { "extendedSession", Value.ForBool(true) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + requestBytes = orchestratorRequest.ToByteArray(); + requestString = Convert.ToBase64String(requestBytes); + stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); + // The extended session is still initialized due to the well-formed extended session timeout key + Assert.True(extendedSessions.IsInitialized()); + Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out object? extendedSession)); + + // Wrong value type for extended session key + orchestratorRequest.Properties.Clear(); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(false) }, + { "ExtendedSession", Value.ForNumber(1) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + requestBytes = orchestratorRequest.ToByteArray(); + requestString = Convert.ToBase64String(requestBytes); + stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); + // The extended session is still initialized due to the well-formed extended session timeout key + Assert.True(extendedSessions.IsInitialized()); + Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out extendedSession)); + + // No extended session key + orchestratorRequest.Properties.Clear(); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(false) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + requestBytes = orchestratorRequest.ToByteArray(); + requestString = Convert.ToBase64String(requestBytes); + stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); + // The extended session is still initialized due to the well-formed extended session timeout key + Assert.True(extendedSessions.IsInitialized()); + Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out extendedSession)); + } + + /// + /// These tests verify that a malformed/nonexistent "IncludePastEvents" parameter means that the worker will attempt to + /// fulfill the orchestration request and not request a history for it. However, it is of course very undesirable that a + /// history is not attached to the request, but no history is requested by the worker due to a malformed "IncludePastEvents" parameter + /// even when it needs one to fulfill the request. This would need to be checked on whatever side is calling this SDK. + /// + [Fact] + public void MalformedPastEventsParameter_Means_NoHistoryRequired() + { + using var extendedSessions = new ExtendedSessionsCache(); + var historyEvent = new Protobuf.HistoryEvent + { + EventId = -1, + Timestamp = Timestamp.FromDateTime(DateTime.UtcNow), + ExecutionStarted = new Protobuf.ExecutionStartedEvent() + { + OrchestrationInstance = new Protobuf.OrchestrationInstance + { + InstanceId = TestInstanceId, + ExecutionId = TestExecutionId, + }, + } + }; + Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([historyEvent]); + + // Misspelled include past events key + orchestratorRequest.Properties.Add(new MapField() { + { "INcludePastEvents", Value.ForBool(false) }, + { "ExtendedSession", Value.ForBool(false) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + byte[] requestBytes = orchestratorRequest.ToByteArray(); + string requestString = Convert.ToBase64String(requestBytes); + string stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + Protobuf.OrchestratorResponse response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); + Assert.False(response.RequiresHistory); + + // Wrong value type for include past events key + orchestratorRequest.Properties.Clear(); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForString("no") }, + { "ExtendedSession", Value.ForBool(false) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + requestBytes = orchestratorRequest.ToByteArray(); + requestString = Convert.ToBase64String(requestBytes); + stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); + Assert.False(response.RequiresHistory); + + // No include past events key + orchestratorRequest.Properties.Clear(); + orchestratorRequest.Properties.Add(new MapField() { + { "ExtendedSession", Value.ForBool(false) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + requestBytes = orchestratorRequest.ToByteArray(); + requestString = Convert.ToBase64String(requestBytes); + stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); + Assert.False(response.RequiresHistory); + } + + [Fact] + public void Incomplete_Orchestration_Stored() + { + using var extendedSessions = new ExtendedSessionsCache(); + var historyEvent = new Protobuf.HistoryEvent + { + EventId = -1, + Timestamp = Timestamp.FromDateTime(DateTime.UtcNow), + ExecutionStarted = new Protobuf.ExecutionStartedEvent() + { + OrchestrationInstance = new Protobuf.OrchestrationInstance + { + InstanceId = TestInstanceId, + ExecutionId = TestExecutionId, + }, + } + }; + Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([historyEvent]); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(true) }, + { "ExtendedSession", Value.ForBool(true) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + byte[] requestBytes = orchestratorRequest.ToByteArray(); + string requestString = Convert.ToBase64String(requestBytes); + GrpcOrchestrationRunner.LoadAndRun(requestString, new CallSubOrchestrationOrchestrator(), extendedSessions); + Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out object? extendedSession)); + } + + [Fact] + public void Complete_Orchestration_NotStored() + { + using var extendedSessions = new ExtendedSessionsCache(); + var historyEvent = new Protobuf.HistoryEvent + { + EventId = -1, + Timestamp = Timestamp.FromDateTime(DateTime.UtcNow), + ExecutionStarted = new Protobuf.ExecutionStartedEvent() + { + OrchestrationInstance = new Protobuf.OrchestrationInstance + { + InstanceId = TestInstanceId, + ExecutionId = TestExecutionId, + }, + } + }; + Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([historyEvent]); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(true) }, + { "ExtendedSession", Value.ForBool(true) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + byte[] requestBytes = orchestratorRequest.ToByteArray(); + string requestString = Convert.ToBase64String(requestBytes); + GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + Assert.True(extendedSessions.IsInitialized()); + Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out object? extendedSession)); + } + + [Fact] + public void ExternallyEndedExtendedSession_Evicted() + { + using var extendedSessions = new ExtendedSessionsCache(); + var historyEvent = new Protobuf.HistoryEvent + { + EventId = -1, + Timestamp = Timestamp.FromDateTime(DateTime.UtcNow), + ExecutionStarted = new Protobuf.ExecutionStartedEvent() + { + OrchestrationInstance = new Protobuf.OrchestrationInstance + { + InstanceId = TestInstanceId, + ExecutionId = TestExecutionId, + }, + } + }; + Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([historyEvent]); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(true) }, + { "ExtendedSession", Value.ForBool(true) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + byte[] requestBytes = orchestratorRequest.ToByteArray(); + string requestString = Convert.ToBase64String(requestBytes); + GrpcOrchestrationRunner.LoadAndRun(requestString, new CallSubOrchestrationOrchestrator(), extendedSessions); + Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out object? extendedSession)); + + // Now set the extended session flag to false for this instance + orchestratorRequest.Properties.Clear(); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(true) }, + { "ExtendedSession", Value.ForBool(false) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + requestBytes = orchestratorRequest.ToByteArray(); + requestString = Convert.ToBase64String(requestBytes); + GrpcOrchestrationRunner.LoadAndRun(requestString, new CallSubOrchestrationOrchestrator(), extendedSessions); + Assert.True(extendedSessions.IsInitialized()); + Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out extendedSession)); + } + + [Fact] + public async void Stale_ExtendedSessions_Evicted_Async() + { + using var extendedSessions = new ExtendedSessionsCache(); + int extendedSessionIdleTimeout = 5; + var historyEvent = new Protobuf.HistoryEvent + { + EventId = -1, + Timestamp = Timestamp.FromDateTime(DateTime.UtcNow), + ExecutionStarted = new Protobuf.ExecutionStartedEvent() + { + OrchestrationInstance = new Protobuf.OrchestrationInstance + { + InstanceId = TestInstanceId, + ExecutionId = TestExecutionId, + }, + } + }; + Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([historyEvent]); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(true) }, + { "ExtendedSession", Value.ForBool(true) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(extendedSessionIdleTimeout) } }); + byte[] requestBytes = orchestratorRequest.ToByteArray(); + string requestString = Convert.ToBase64String(requestBytes); + GrpcOrchestrationRunner.LoadAndRun(requestString, new CallSubOrchestrationOrchestrator(), extendedSessions); + Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.GetOrInitializeCache(extendedSessionIdleTimeout).TryGetValue(TestInstanceId, out object? extendedSession)); + + // Wait for longer than the timeout to account for finite cache scan for stale items frequency + await Task.Delay(extendedSessionIdleTimeout * 1000 * 2); + Assert.False(extendedSessions.GetOrInitializeCache(extendedSessionIdleTimeout).TryGetValue(TestInstanceId, out extendedSession)); + + // Now that the entry was evicted from the cache, the orchestration runner needs an orchestration history to complete the request + orchestratorRequest.Properties.Clear(); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(false) }, + { "ExtendedSession", Value.ForBool(true) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(extendedSessionIdleTimeout) } }); + requestBytes = orchestratorRequest.ToByteArray(); + requestString = Convert.ToBase64String(requestBytes); + string stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new CallSubOrchestrationOrchestrator(), extendedSessions); + Protobuf.OrchestratorResponse response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); + Assert.True(response.RequiresHistory); + } + + static Protobuf.OrchestratorRequest CreateOrchestratorRequest(IEnumerable newEvents) + { + var orchestratorRequest = new Protobuf.OrchestratorRequest() + { + InstanceId = TestInstanceId, + PastEvents = { Enumerable.Empty() }, + NewEvents = { newEvents }, + EntityParameters = new Protobuf.OrchestratorEntityParameters + { + EntityMessageReorderWindow = Duration.FromTimeSpan(TimeSpan.Zero), + }, + }; + return orchestratorRequest; + } + + class SimpleOrchestrator : TaskOrchestrator + { + public override Task RunAsync(TaskOrchestrationContext context, string input) + { + return Task.FromResult(input); + } + } + + class CallSubOrchestrationOrchestrator : TaskOrchestrator + { + public override async Task RunAsync(TaskOrchestrationContext context, string input) + { + await context.CallSubOrchestratorAsync(nameof(SimpleOrchestrator)); + return input; + } + } +} diff --git a/test/Worker/Core.Tests/Worker.Tests.csproj b/test/Worker/Core.Tests/Worker.Tests.csproj index 736986d4d..b9de90994 100644 --- a/test/Worker/Core.Tests/Worker.Tests.csproj +++ b/test/Worker/Core.Tests/Worker.Tests.csproj @@ -10,6 +10,7 @@ + From 1c38f81dfaf62b46e3a288f4005d8b1e2664374a Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Wed, 6 Aug 2025 15:24:19 -0700 Subject: [PATCH 25/40] had the wrong default timeout --- test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs b/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs index 7dbfb6d35..7b0878eab 100644 --- a/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs +++ b/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs @@ -11,7 +11,7 @@ public class GrpcOrchestrationRunnerTests { const string TestInstanceId = "instance_id"; const string TestExecutionId = "execution_id"; - const int DefaultExtendedSessionIdleTimeoutInSeconds = 300; + const int DefaultExtendedSessionIdleTimeoutInSeconds = 30; [Fact] public void EmptyOrNullParameters_Throw() From 5a762a38aca33ff5ebee4f6a60ab89f322550600 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Wed, 6 Aug 2025 15:33:09 -0700 Subject: [PATCH 26/40] fixed failing test --- test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs b/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs index 7b0878eab..adf2c7c68 100644 --- a/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs +++ b/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs @@ -39,11 +39,9 @@ public void EmptyHistory_Returns_NeedsHistoryInResponse() using var extendedSessions = new ExtendedSessionsCache(); // No history and without extended sessions enabled - var orchestratorRequest = CreateOrchestratorRequest([]); + Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([]); orchestratorRequest.Properties.Add(new MapField() { - { "IncludePastEvents", Value.ForBool(false) }, - { "ExtendedSession", Value.ForBool(false) }, - { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + { "IncludePastEvents", Value.ForBool(false) }}); byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); string stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); @@ -52,7 +50,6 @@ public void EmptyHistory_Returns_NeedsHistoryInResponse() Assert.False(extendedSessions.IsInitialized()); // No history but with extended sessions enabled - orchestratorRequest.Properties.Clear(); orchestratorRequest.Properties.Add(new MapField() { { "ExtendedSession", Value.ForBool(true) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); @@ -68,7 +65,7 @@ public void EmptyHistory_Returns_NeedsHistoryInResponse() public void MalformedRequestParameters_Means_NoExtendedSessionsStored() { using var extendedSessions = new ExtendedSessionsCache(); - var orchestratorRequest = CreateOrchestratorRequest([]); + Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([]); // Misspelled extended session timeout key orchestratorRequest.Properties.Add(new MapField() { From 083a304789d3f519a86d6eef17327941bdc41240 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 8 Aug 2025 21:20:05 -0700 Subject: [PATCH 27/40] adding new dependencies and new package number --- Directory.Packages.props | 2 +- eng/targets/Release.props | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d9e70ac48..3d8cbad67 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,7 +29,7 @@ - + diff --git a/eng/targets/Release.props b/eng/targets/Release.props index 860292f49..d98052b86 100644 --- a/eng/targets/Release.props +++ b/eng/targets/Release.props @@ -17,8 +17,8 @@ - 1.12.1 - private2 + 1.13.0 + private1 From c0af2868cb0397d42e842eba5b4a067c028d198d Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Wed, 20 Aug 2025 18:01:18 -0700 Subject: [PATCH 28/40] pushing bug fix for not honoring a host restarting an extended session --- src/Worker/Grpc/GrpcOrchestrationRunner.cs | 30 ++++++++++------ .../GrpcOrchestrationRunnerTests.cs | 35 +++++++++++++++++++ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index 8fa3d6312..c0c79a428 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -170,11 +170,14 @@ public static string LoadAndRun( pair => pair.Key, pair => ProtoUtils.ConvertValueToObject(pair.Value)); - OrchestratorExecutionResult? result = null; + OrchestratorExecutionResult? result = null; + MemoryCache? extendedSessions = null; + + // If any of the request parameters are malformed, we assume the default - extended sessions are not enabled and the orchestration history is attached bool addToExtendedSessions = false; bool requiresHistory = false; + bool pastEventsIncluded = true; double extendedSessionIdleTimeoutInSeconds = 0; - MemoryCache? extendedSessions = null; if (properties.TryGetValue("ExtendedSessionIdleTimeoutInSeconds", out object? extendedSessionIdleTimeoutObj) && extendedSessionIdleTimeoutObj is double extendedSessionIdleTimeout @@ -183,13 +186,21 @@ public static string LoadAndRun( extendedSessionIdleTimeoutInSeconds = extendedSessionIdleTimeout; extendedSessions = extendedSessionsCache.GetOrInitializeCache(extendedSessionIdleTimeoutInSeconds); } + + if (properties.TryGetValue("IncludePastEvents", out object? includePastEventsObj) + && includePastEventsObj is bool includePastEvents) + { + pastEventsIncluded = includePastEvents; + } if (properties.TryGetValue("ExtendedSession", out object? isExtendedSessionObj) && isExtendedSessionObj is bool isExtendedSession && isExtendedSession && extendedSessions != null) - { - if (extendedSessions.TryGetValue(request.InstanceId, out ExtendedSessionState? extendedSessionState) && extendedSessionState is not null) + { + // If a history was provided, even if we already have an extended session stored, we always want to evict whatever state is in the cache and replace it with a new extended + // session based on the provided history + if (!pastEventsIncluded && extendedSessions.TryGetValue(request.InstanceId, out ExtendedSessionState? extendedSessionState) && extendedSessionState is not null) { OrchestrationRuntimeState runtimeState = extendedSessionState!.RuntimeState; runtimeState.NewEvents.Clear(); @@ -205,20 +216,17 @@ public static string LoadAndRun( } } else - { + { + extendedSessions.Remove(request.InstanceId); addToExtendedSessions = true; } } if (result == null) { - // If this is the first orchestration execution, then the past events count will be 0 but includePastEvents will be true (there are just none to include). - // Otherwise, there is an orchestration history but DurableTask.Core did not attach it since the extended session is still active on its end, but we have since evicted the + // DurableTask.Core did not attach the orchestration history since the extended session is still active on its end, but we have since evicted the // session and lost the orchestration history so we cannot replay the orchestration. - if (pastEvents.Count == 0 - && (properties.TryGetValue("IncludePastEvents", out object? pastEventsIncludedObj) - && pastEventsIncludedObj is bool pastEventsIncluded - && !pastEventsIncluded)) + if (!pastEventsIncluded) { requiresHistory = true; } diff --git a/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs b/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs index adf2c7c68..c2b3ba3f9 100644 --- a/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs +++ b/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs @@ -346,6 +346,41 @@ public async void Stale_ExtendedSessions_Evicted_Async() Assert.True(response.RequiresHistory); } + [Fact] + public void PastEventIncludes_Means_ExtendedSession_Evicted() + { + using var extendedSessions = new ExtendedSessionsCache(); + int extendedSessionIdleTimeout = 5; + var historyEvent = new Protobuf.HistoryEvent + { + EventId = -1, + Timestamp = Timestamp.FromDateTime(DateTime.UtcNow), + ExecutionStarted = new Protobuf.ExecutionStartedEvent() + { + OrchestrationInstance = new Protobuf.OrchestrationInstance + { + InstanceId = TestInstanceId, + ExecutionId = TestExecutionId, + }, + } + }; + Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([historyEvent]); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(true) }, + { "ExtendedSession", Value.ForBool(true) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(extendedSessionIdleTimeout) } }); + byte[] requestBytes = orchestratorRequest.ToByteArray(); + string requestString = Convert.ToBase64String(requestBytes); + GrpcOrchestrationRunner.LoadAndRun(requestString, new CallSubOrchestrationOrchestrator(), extendedSessions); + Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.GetOrInitializeCache(extendedSessionIdleTimeout).TryGetValue(TestInstanceId, out object? extendedSession)); + + // Now we will retry the same exact request. If the extended session is not evicted, then the request will fail due to duplicate ExecutionStarted events being detected + // If the extended session is evicted because IncludePastEvents is true, then the request will succeed and a new extended session will be stored + GrpcOrchestrationRunner.LoadAndRun(requestString, new CallSubOrchestrationOrchestrator(), extendedSessions); + Assert.True(extendedSessions.GetOrInitializeCache(extendedSessionIdleTimeout).TryGetValue(TestInstanceId, out extendedSession)); + } + static Protobuf.OrchestratorRequest CreateOrchestratorRequest(IEnumerable newEvents) { var orchestratorRequest = new Protobuf.OrchestratorRequest() From 527cf6db423fa1d9fdf63bec0e552db83c4e8f1a Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Wed, 20 Aug 2025 18:07:47 -0700 Subject: [PATCH 29/40] updating version numbers --- Directory.Packages.props | 2 +- eng/targets/Release.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3d8cbad67..09b06c520 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,7 +29,7 @@ - + diff --git a/eng/targets/Release.props b/eng/targets/Release.props index d98052b86..39e303492 100644 --- a/eng/targets/Release.props +++ b/eng/targets/Release.props @@ -17,7 +17,7 @@ - 1.13.0 + 1.14.0 private1 From 0604373c59aa268a131f8539df92c3fccf228337 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Wed, 20 Aug 2025 19:14:40 -0700 Subject: [PATCH 30/40] added proto changes from main branch --- src/Grpc/orchestrator_service.proto | 33 +++++++++++++++++++---------- src/Grpc/versions.txt | 4 ++-- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/Grpc/orchestrator_service.proto b/src/Grpc/orchestrator_service.proto index 8b22a850a..79b9ceda1 100644 --- a/src/Grpc/orchestrator_service.proto +++ b/src/Grpc/orchestrator_service.proto @@ -193,7 +193,7 @@ message EntityOperationCalledEvent { } message EntityLockRequestedEvent { - string criticalSectionId = 1; + string criticalSectionId = 1; repeated string lockSet = 2; int32 position = 3; google.protobuf.StringValue parentInstanceId = 4; // used only within messages, null in histories @@ -218,7 +218,7 @@ message EntityUnlockSentEvent { message EntityLockGrantedEvent { string criticalSectionId = 1; } - + message HistoryEvent { int32 eventId = 1; google.protobuf.Timestamp timestamp = 2; @@ -245,8 +245,8 @@ message HistoryEvent { ExecutionResumedEvent executionResumed = 22; EntityOperationSignaledEvent entityOperationSignaled = 23; EntityOperationCalledEvent entityOperationCalled = 24; - EntityOperationCompletedEvent entityOperationCompleted = 25; - EntityOperationFailedEvent entityOperationFailed = 26; + EntityOperationCompletedEvent entityOperationCompleted = 25; + EntityOperationFailedEvent entityOperationFailed = 26; EntityLockRequestedEvent entityLockRequested = 27; EntityLockGrantedEvent entityLockGranted = 28; EntityUnlockSentEvent entityUnlockSent = 29; @@ -258,6 +258,7 @@ message ScheduleTaskAction { google.protobuf.StringValue version = 2; google.protobuf.StringValue input = 3; map tags = 4; + TraceContext parentTraceContext = 5; } message CreateSubOrchestrationAction { @@ -265,6 +266,7 @@ message CreateSubOrchestrationAction { string name = 2; google.protobuf.StringValue version = 3; google.protobuf.StringValue input = 4; + TraceContext parentTraceContext = 5; } message CreateTimerAction { @@ -314,6 +316,11 @@ message OrchestratorAction { } } +message OrchestrationTraceContext { + google.protobuf.StringValue spanID = 1; + google.protobuf.Timestamp spanStartTime = 2; +} + message OrchestratorRequest { string instanceId = 1; google.protobuf.StringValue executionId = 2; @@ -322,6 +329,8 @@ message OrchestratorRequest { OrchestratorEntityParameters entityParameters = 5; bool requiresHistoryStreaming = 6; map properties = 7; + + OrchestrationTraceContext orchestrationTraceContext = 8; } message OrchestratorResponse { @@ -333,8 +342,10 @@ message OrchestratorResponse { // The number of work item events that were processed by the orchestrator. // This field is optional. If not set, the service should assume that the orchestrator processed all events. google.protobuf.Int32Value numEventsProcessed = 5; + OrchestrationTraceContext orchestrationTraceContext = 6; + // Whether or not a history is required to complete the original OrchestratorRequest and none was provided. - bool requiresHistory = 6; + bool requiresHistory = 7; } message CreateInstanceRequest { @@ -500,7 +511,7 @@ message SignalEntityRequest { } message SignalEntityResponse { - // no payload + // no payload } message GetEntityRequest { @@ -675,16 +686,16 @@ service TaskHubSidecarService { // Waits for an orchestration instance to reach a running or completion state. rpc WaitForInstanceStart(GetInstanceRequest) returns (GetInstanceResponse); - + // Waits for an orchestration instance to reach a completion state (completed, failed, terminated, etc.). rpc WaitForInstanceCompletion(GetInstanceRequest) returns (GetInstanceResponse); // Raises an event to a running orchestration instance. rpc RaiseEvent(RaiseEventRequest) returns (RaiseEventResponse); - + // Terminates a running orchestration instance. rpc TerminateInstance(TerminateRequest) returns (TerminateResponse); - + // Suspends a running orchestration instance. rpc SuspendInstance(SuspendRequest) returns (SuspendResponse); @@ -766,7 +777,7 @@ message CompleteTaskResponse { } message HealthPing { - // No payload + // No payload } message StreamInstanceHistoryRequest { @@ -779,4 +790,4 @@ message StreamInstanceHistoryRequest { message HistoryChunk { repeated HistoryEvent events = 1; -} +} \ No newline at end of file diff --git a/src/Grpc/versions.txt b/src/Grpc/versions.txt index 95e4e8841..59436cdf8 100644 --- a/src/Grpc/versions.txt +++ b/src/Grpc/versions.txt @@ -1,2 +1,2 @@ -# The following files were downloaded from branch stevosyan/extended-sessions-for-orchestrations-isolated at 2025-08-21 02:06:15 UTC -https://raw.githubusercontent.com/microsoft/durabletask-protobuf/930a06ef7df4f3156fdaa619e8e4d6439d43fcb5/protos/orchestrator_service.proto +# The following files were downloaded from branch stevosyan/extended-sessions-for-orchestrations-isolated at 2025-08-21 02:14:04 UTC +https://raw.githubusercontent.com/microsoft/durabletask-protobuf/42cc3e416e9b227ed0a68c7dfd504ab5aed84e26/protos/orchestrator_service.proto From 8e69683ffdc1a3e54fbbac70f9a336fa1e5c7b47 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Thu, 21 Aug 2025 11:12:05 -0700 Subject: [PATCH 31/40] downgrading the cache package to a version that is test with net6.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 09b06c520..4ea8da0f1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,7 +9,7 @@ - + From 547261eac81765167e23979cff7ed044dba64b67 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Thu, 11 Sep 2025 12:09:13 -0700 Subject: [PATCH 32/40] reverting ci changes --- Directory.Packages.props | 3 +-- eng/targets/Release.props | 4 ++-- eng/templates/build.yml | 7 ++++--- nuget.config | 21 ++++++--------------- src/Grpc/orchestrator_service.proto | 4 +--- src/Grpc/versions.txt | 4 ++-- 6 files changed, 16 insertions(+), 27 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4ea8da0f1..8388030fa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,7 +9,6 @@ - @@ -29,7 +28,7 @@ - + diff --git a/eng/targets/Release.props b/eng/targets/Release.props index 39e303492..3a9b61267 100644 --- a/eng/targets/Release.props +++ b/eng/targets/Release.props @@ -17,8 +17,8 @@ - 1.14.0 - private1 + 1.13.0 + diff --git a/eng/templates/build.yml b/eng/templates/build.yml index 713fc63ff..a94d3aff0 100644 --- a/eng/templates/build.yml +++ b/eng/templates/build.yml @@ -41,10 +41,11 @@ jobs: - task: DotNetCoreCLI@2 displayName: Restore inputs: - command: custom - custom: restore + command: restore + verbosityRestore: Minimal projects: $(project) - arguments: '-v m' + feedsToUse: config + nugetConfigPath: nuget.config # Build source directory - task: DotNetCoreCLI@2 diff --git a/nuget.config b/nuget.config index 719044ea2..6f5093913 100644 --- a/nuget.config +++ b/nuget.config @@ -1,16 +1,7 @@ - - - - - - - - - - - - - - - + + + + + + \ No newline at end of file diff --git a/src/Grpc/orchestrator_service.proto b/src/Grpc/orchestrator_service.proto index 79b9ceda1..95bfeedc8 100644 --- a/src/Grpc/orchestrator_service.proto +++ b/src/Grpc/orchestrator_service.proto @@ -342,10 +342,8 @@ message OrchestratorResponse { // The number of work item events that were processed by the orchestrator. // This field is optional. If not set, the service should assume that the orchestrator processed all events. google.protobuf.Int32Value numEventsProcessed = 5; - OrchestrationTraceContext orchestrationTraceContext = 6; - // Whether or not a history is required to complete the original OrchestratorRequest and none was provided. - bool requiresHistory = 7; + OrchestrationTraceContext orchestrationTraceContext = 6; } message CreateInstanceRequest { diff --git a/src/Grpc/versions.txt b/src/Grpc/versions.txt index 59436cdf8..5c0de3577 100644 --- a/src/Grpc/versions.txt +++ b/src/Grpc/versions.txt @@ -1,2 +1,2 @@ -# The following files were downloaded from branch stevosyan/extended-sessions-for-orchestrations-isolated at 2025-08-21 02:14:04 UTC -https://raw.githubusercontent.com/microsoft/durabletask-protobuf/42cc3e416e9b227ed0a68c7dfd504ab5aed84e26/protos/orchestrator_service.proto +# The following files were downloaded from branch main at 2025-08-08 16:46:11 UTC +https://raw.githubusercontent.com/microsoft/durabletask-protobuf/e88acbd07ae38b499dbe8c4e333e9e3feeb2a9cc/protos/orchestrator_service.proto From 44d654f1a4284de37e860d46b0c395e66b46f074 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Thu, 11 Sep 2025 12:11:15 -0700 Subject: [PATCH 33/40] adding caching package --- Directory.Packages.props | 1 + 1 file changed, 1 insertion(+) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8388030fa..f606ed6d2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,6 +9,7 @@ + From d11758c2f5cda7abca600c2e85f0565eba56632e Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Thu, 11 Sep 2025 20:05:13 -0700 Subject: [PATCH 34/40] addressing PR comments --- .../{Grpc => Core}/ExtendedSessionState.cs | 12 ++-- .../{Grpc => Core}/ExtendedSessionsCache.cs | 17 ++--- src/Worker/Core/Worker.csproj | 1 + src/Worker/Grpc/GrpcOrchestrationRunner.cs | 60 +++------------- src/Worker/Grpc/Worker.Grpc.csproj | 4 -- test/Worker/Core.Tests/Worker.Tests.csproj | 1 - .../GrpcOrchestrationRunnerTests.cs | 68 +++++++++++++------ 7 files changed, 70 insertions(+), 93 deletions(-) rename src/Worker/{Grpc => Core}/ExtendedSessionState.cs (70%) rename src/Worker/{Grpc => Core}/ExtendedSessionsCache.cs (86%) rename test/Worker/{Core.Tests => Grpc.Tests}/GrpcOrchestrationRunnerTests.cs (88%) diff --git a/src/Worker/Grpc/ExtendedSessionState.cs b/src/Worker/Core/ExtendedSessionState.cs similarity index 70% rename from src/Worker/Grpc/ExtendedSessionState.cs rename to src/Worker/Core/ExtendedSessionState.cs index 13b5d05f7..4d67e0476 100644 --- a/src/Worker/Grpc/ExtendedSessionState.cs +++ b/src/Worker/Core/ExtendedSessionState.cs @@ -3,12 +3,12 @@ using DurableTask.Core; -namespace Microsoft.DurableTask.Worker.Grpc; +namespace Microsoft.DurableTask.Worker; /// /// Represents the state of an extended session for an orchestration. /// -class ExtendedSessionState +public class ExtendedSessionState { /// /// Initializes a new instance of the class. @@ -16,7 +16,7 @@ class ExtendedSessionState /// The orchestration's runtime state. /// The TaskOrchestration implementation of the orchestration. /// The TaskOrchestrationExecutor for the orchestration. - internal ExtendedSessionState(OrchestrationRuntimeState state, TaskOrchestration taskOrchestration, TaskOrchestrationExecutor orchestrationExecutor) + public ExtendedSessionState(OrchestrationRuntimeState state, TaskOrchestration taskOrchestration, TaskOrchestrationExecutor orchestrationExecutor) { this.RuntimeState = state; this.TaskOrchestration = taskOrchestration; @@ -26,15 +26,15 @@ internal ExtendedSessionState(OrchestrationRuntimeState state, TaskOrchestration /// /// Gets or sets the saved runtime state of the orchestration. /// - internal OrchestrationRuntimeState RuntimeState { get; set; } + public OrchestrationRuntimeState RuntimeState { get; set; } /// /// Gets or sets the saved TaskOrchestration implementation of the orchestration. /// - internal TaskOrchestration TaskOrchestration { get; set; } + public TaskOrchestration TaskOrchestration { get; set; } /// /// Gets or sets the saved TaskOrchestrationExecutor. /// - internal TaskOrchestrationExecutor OrchestrationExecutor { get; set; } + public TaskOrchestrationExecutor OrchestrationExecutor { get; set; } } diff --git a/src/Worker/Grpc/ExtendedSessionsCache.cs b/src/Worker/Core/ExtendedSessionsCache.cs similarity index 86% rename from src/Worker/Grpc/ExtendedSessionsCache.cs rename to src/Worker/Core/ExtendedSessionsCache.cs index 90254c1c5..59df25366 100644 --- a/src/Worker/Grpc/ExtendedSessionsCache.cs +++ b/src/Worker/Core/ExtendedSessionsCache.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Caching.Memory; -namespace Microsoft.DurableTask.Worker.Grpc; +namespace Microsoft.DurableTask.Worker; /// /// A cache for extended sessions that wraps a instance. @@ -13,6 +13,12 @@ public class ExtendedSessionsCache : IDisposable { MemoryCache? extendedSessions; + /// + /// Gets a value indicating whether returns whether or not the cache has been initialized. + /// + /// True if the cache has been initialized, false otherwise. + public bool IsInitialized => this.extendedSessions is not null; + /// /// Dispose the cache and release all resources. /// @@ -39,13 +45,4 @@ public MemoryCache GetOrInitializeCache(double expirationScanFrequencyInSeconds) return this.extendedSessions; } - - /// - /// Returns whether or not the cache has been initialized. - /// - /// True if the cache has been initialized, false otherwise. - public bool IsInitialized() - { - return this.extendedSessions is not null; - } } diff --git a/src/Worker/Core/Worker.csproj b/src/Worker/Core/Worker.csproj index 5791edf73..daed82b15 100644 --- a/src/Worker/Core/Worker.csproj +++ b/src/Worker/Core/Worker.csproj @@ -9,6 +9,7 @@ The worker is responsible for processing durable task work items. + diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index b077e24a5..7a3758e12 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -84,50 +84,9 @@ public static string LoadAndRun( ITaskOrchestrator implementation, IServiceProvider? services = null) { - Check.NotNullOrEmpty(encodedOrchestratorRequest); - Check.NotNull(implementation); - - P.OrchestratorRequest request = P.OrchestratorRequest.Parser.Base64Decode( - encodedOrchestratorRequest); - - List pastEvents = request.PastEvents.Select(ProtoUtils.ConvertHistoryEvent).ToList(); - IEnumerable newEvents = request.NewEvents.Select(ProtoUtils.ConvertHistoryEvent); - Dictionary properties = request.Properties.ToDictionary( - pair => pair.Key, - pair => ProtoUtils.ConvertValueToObject(pair.Value)); - - // Re-construct the orchestration state from the history. - // New events must be added using the AddEvent method. - OrchestrationRuntimeState runtimeState = new(pastEvents); - foreach (HistoryEvent newEvent in newEvents) - { - runtimeState.AddEvent(newEvent); - } - - TaskName orchestratorName = new(runtimeState.Name); - ParentOrchestrationInstance? parent = runtimeState.ParentInstance is ParentInstance p - ? new(new(p.Name), p.OrchestrationInstance.InstanceId) - : null; - - DurableTaskShimFactory factory = services is null - ? DurableTaskShimFactory.Default - : ActivatorUtilities.GetServiceOrCreateInstance(services); - TaskOrchestration shim = factory.CreateOrchestration(orchestratorName, implementation, properties, parent); - TaskOrchestrationExecutor executor = new(runtimeState, shim, BehaviorOnContinueAsNew.Carryover, request.EntityParameters.ToCore(), ErrorPropagationMode.UseFailureDetails); - OrchestratorExecutionResult result = executor.Execute(); - - P.OrchestratorResponse response = ProtoUtils.ConstructOrchestratorResponse( - request.InstanceId, - request.ExecutionId, - result.CustomStatus, - result.Actions, - completionToken: string.Empty, /* doesn't apply */ - entityConversionState: null, - orchestrationActivity: null); - byte[] responseBytes = response.ToByteArray(); - return Convert.ToBase64String(responseBytes); - } - + return LoadAndRun(encodedOrchestratorRequest, implementation, services); + } + /// /// Deserializes orchestration history from and uses it to resume the /// orchestrator implemented by . @@ -136,8 +95,8 @@ public static string LoadAndRun( /// The encoded protobuf payload representing an orchestration execution request. This is a base64-encoded string. /// /// - /// An implementation that defines the orchestrator logic. - /// + /// An implementation that defines the orchestrator logic. + /// /// /// The cache of extended sessions which can be used to retrieve the if this orchestration request is from within an extended session. /// @@ -155,13 +114,12 @@ public static string LoadAndRun( /// public static string LoadAndRun( string encodedOrchestratorRequest, - ITaskOrchestrator implementation, - ExtendedSessionsCache extendedSessionsCache, + ITaskOrchestrator implementation, + ExtendedSessionsCache? extendedSessionsCache, IServiceProvider? services = null) { Check.NotNullOrEmpty(encodedOrchestratorRequest); - Check.NotNull(implementation); - Check.NotNull(extendedSessionsCache); + Check.NotNull(implementation); P.OrchestratorRequest request = P.OrchestratorRequest.Parser.Base64Decode( encodedOrchestratorRequest); @@ -186,7 +144,7 @@ public static string LoadAndRun( && extendedSessionIdleTimeout >= 0) { extendedSessionIdleTimeoutInSeconds = extendedSessionIdleTimeout; - extendedSessions = extendedSessionsCache.GetOrInitializeCache(extendedSessionIdleTimeoutInSeconds); + extendedSessions = extendedSessionsCache?.GetOrInitializeCache(extendedSessionIdleTimeoutInSeconds); } if (properties.TryGetValue("IncludePastEvents", out object? includePastEventsObj) diff --git a/src/Worker/Grpc/Worker.Grpc.csproj b/src/Worker/Grpc/Worker.Grpc.csproj index 5d998be06..991fef8f1 100644 --- a/src/Worker/Grpc/Worker.Grpc.csproj +++ b/src/Worker/Grpc/Worker.Grpc.csproj @@ -6,10 +6,6 @@ true - - - - diff --git a/test/Worker/Core.Tests/Worker.Tests.csproj b/test/Worker/Core.Tests/Worker.Tests.csproj index b9de90994..736986d4d 100644 --- a/test/Worker/Core.Tests/Worker.Tests.csproj +++ b/test/Worker/Core.Tests/Worker.Tests.csproj @@ -10,7 +10,6 @@ - diff --git a/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs b/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs similarity index 88% rename from test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs rename to test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs index c2b3ba3f9..58d39c9fa 100644 --- a/test/Worker/Core.Tests/GrpcOrchestrationRunnerTests.cs +++ b/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs @@ -4,9 +4,9 @@ using Google.Protobuf; using Google.Protobuf.Collections; using Google.Protobuf.WellKnownTypes; -using Microsoft.DurableTask.Worker.Grpc; -namespace Microsoft.DurableTask.Worker.Tests; +namespace Microsoft.DurableTask.Worker.Grpc.Tests; + public class GrpcOrchestrationRunnerTests { const string TestInstanceId = "instance_id"; @@ -14,7 +14,7 @@ public class GrpcOrchestrationRunnerTests const int DefaultExtendedSessionIdleTimeoutInSeconds = 30; [Fact] - public void EmptyOrNullParameters_Throw() + public void EmptyOrNullParameters_Throw_Exceptions() { Action act = () => GrpcOrchestrationRunner.LoadAndRun(string.Empty, new SimpleOrchestrator(), new ExtendedSessionsCache()); @@ -27,10 +27,6 @@ public void EmptyOrNullParameters_Throw() act = () => GrpcOrchestrationRunner.LoadAndRun("request", null!, new ExtendedSessionsCache()); act.Should().ThrowExactly().WithParameterName("implementation"); - - act = () => - GrpcOrchestrationRunner.LoadAndRun("request", new SimpleOrchestrator(), extendedSessionsCache: null!); - act.Should().ThrowExactly().WithParameterName("extendedSessionsCache"); } [Fact] @@ -47,7 +43,7 @@ public void EmptyHistory_Returns_NeedsHistoryInResponse() string stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); Protobuf.OrchestratorResponse response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); Assert.True(response.RequiresHistory); - Assert.False(extendedSessions.IsInitialized()); + Assert.False(extendedSessions.IsInitialized); // No history but with extended sessions enabled orchestratorRequest.Properties.Add(new MapField() { @@ -58,7 +54,7 @@ public void EmptyHistory_Returns_NeedsHistoryInResponse() stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); Assert.True(response.RequiresHistory); - Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.IsInitialized); } [Fact] @@ -76,7 +72,7 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() string requestString = Convert.ToBase64String(requestBytes); string stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); Protobuf.OrchestratorResponse response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); - Assert.False(extendedSessions.IsInitialized()); + Assert.False(extendedSessions.IsInitialized); // Wrong value type for extended session timeout key orchestratorRequest.Properties.Clear(); @@ -87,7 +83,7 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); - Assert.False(extendedSessions.IsInitialized()); + Assert.False(extendedSessions.IsInitialized); // No extended session timeout key orchestratorRequest.Properties.Clear(); @@ -97,7 +93,7 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); - Assert.False(extendedSessions.IsInitialized()); + Assert.False(extendedSessions.IsInitialized); // Misspelled extended session key orchestratorRequest.Properties.Clear(); @@ -110,7 +106,7 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); // The extended session is still initialized due to the well-formed extended session timeout key - Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.IsInitialized); Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out object? extendedSession)); // Wrong value type for extended session key @@ -124,7 +120,7 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); // The extended session is still initialized due to the well-formed extended session timeout key - Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.IsInitialized); Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out extendedSession)); // No extended session key @@ -137,7 +133,7 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); // The extended session is still initialized due to the well-formed extended session timeout key - Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.IsInitialized); Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out extendedSession)); } @@ -226,7 +222,7 @@ public void Incomplete_Orchestration_Stored() byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); GrpcOrchestrationRunner.LoadAndRun(requestString, new CallSubOrchestrationOrchestrator(), extendedSessions); - Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.IsInitialized); Assert.True(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out object? extendedSession)); } @@ -255,7 +251,7 @@ public void Complete_Orchestration_NotStored() byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); - Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.IsInitialized); Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out object? extendedSession)); } @@ -284,7 +280,7 @@ public void ExternallyEndedExtendedSession_Evicted() byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); GrpcOrchestrationRunner.LoadAndRun(requestString, new CallSubOrchestrationOrchestrator(), extendedSessions); - Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.IsInitialized); Assert.True(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out object? extendedSession)); // Now set the extended session flag to false for this instance @@ -296,7 +292,7 @@ public void ExternallyEndedExtendedSession_Evicted() requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); GrpcOrchestrationRunner.LoadAndRun(requestString, new CallSubOrchestrationOrchestrator(), extendedSessions); - Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.IsInitialized); Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out extendedSession)); } @@ -326,7 +322,7 @@ public async void Stale_ExtendedSessions_Evicted_Async() byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); GrpcOrchestrationRunner.LoadAndRun(requestString, new CallSubOrchestrationOrchestrator(), extendedSessions); - Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.IsInitialized); Assert.True(extendedSessions.GetOrInitializeCache(extendedSessionIdleTimeout).TryGetValue(TestInstanceId, out object? extendedSession)); // Wait for longer than the timeout to account for finite cache scan for stale items frequency @@ -372,7 +368,7 @@ public void PastEventIncludes_Means_ExtendedSession_Evicted() byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); GrpcOrchestrationRunner.LoadAndRun(requestString, new CallSubOrchestrationOrchestrator(), extendedSessions); - Assert.True(extendedSessions.IsInitialized()); + Assert.True(extendedSessions.IsInitialized); Assert.True(extendedSessions.GetOrInitializeCache(extendedSessionIdleTimeout).TryGetValue(TestInstanceId, out object? extendedSession)); // Now we will retry the same exact request. If the extended session is not evicted, then the request will fail due to duplicate ExecutionStarted events being detected @@ -381,6 +377,36 @@ public void PastEventIncludes_Means_ExtendedSession_Evicted() Assert.True(extendedSessions.GetOrInitializeCache(extendedSessionIdleTimeout).TryGetValue(TestInstanceId, out extendedSession)); } + [Fact] + public void Null_ExtendedSessionsCache_IsOkay() + { + var historyEvent = new Protobuf.HistoryEvent + { + EventId = -1, + Timestamp = Timestamp.FromDateTime(DateTime.UtcNow), + ExecutionStarted = new Protobuf.ExecutionStartedEvent() + { + OrchestrationInstance = new Protobuf.OrchestrationInstance + { + InstanceId = TestInstanceId, + ExecutionId = TestExecutionId, + }, + } + }; + Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([historyEvent]); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(true) }, + { "ExtendedSession", Value.ForBool(true) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + byte[] requestBytes = orchestratorRequest.ToByteArray(); + string requestString = Convert.ToBase64String(requestBytes); + string stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator()); + Protobuf.OrchestratorResponse response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); + Assert.Single(response.Actions); + Assert.NotNull(response.Actions[0].CompleteOrchestration); + Assert.Equal(Protobuf.OrchestrationStatus.Completed, response.Actions[0].CompleteOrchestration.OrchestrationStatus); + } + static Protobuf.OrchestratorRequest CreateOrchestratorRequest(IEnumerable newEvents) { var orchestratorRequest = new Protobuf.OrchestratorRequest() From bbe2d2d7f0fa5d8b980ea2883e8c4e1e97e47b68 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Thu, 11 Sep 2025 20:06:26 -0700 Subject: [PATCH 35/40] missed one file --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f606ed6d2..4beaa456f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,7 +9,7 @@ - + From dcbf62b9ed657618c3c53115d26473e9cabebd1b Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Thu, 11 Sep 2025 20:09:27 -0700 Subject: [PATCH 36/40] had a mistake in the LoadAndRun path --- src/Worker/Grpc/GrpcOrchestrationRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index 7a3758e12..c1767cd51 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -84,7 +84,7 @@ public static string LoadAndRun( ITaskOrchestrator implementation, IServiceProvider? services = null) { - return LoadAndRun(encodedOrchestratorRequest, implementation, services); + return LoadAndRun(encodedOrchestratorRequest, implementation, extendedSessionsCache: null, services: services); } /// From 9a573aae9532ae6172514daac94518f378fe0753 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Thu, 11 Sep 2025 23:06:56 -0700 Subject: [PATCH 37/40] slight update to extended session parameter name --- src/Worker/Grpc/GrpcOrchestrationRunner.cs | 2 +- .../GrpcOrchestrationRunnerTests.cs | 37 ++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index c1767cd51..da9dce90d 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -153,7 +153,7 @@ public static string LoadAndRun( pastEventsIncluded = includePastEvents; } - if (properties.TryGetValue("ExtendedSession", out object? isExtendedSessionObj) + if (properties.TryGetValue("IsExtendedSession", out object? isExtendedSessionObj) && isExtendedSessionObj is bool isExtendedSession && isExtendedSession && extendedSessions != null) diff --git a/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs b/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs index 58d39c9fa..90c1a20d3 100644 --- a/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs +++ b/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs @@ -47,7 +47,7 @@ public void EmptyHistory_Returns_NeedsHistoryInResponse() // No history but with extended sessions enabled orchestratorRequest.Properties.Add(new MapField() { - { "ExtendedSession", Value.ForBool(true) }, + { "IsExtendedSession", Value.ForBool(true) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); @@ -66,7 +66,7 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() // Misspelled extended session timeout key orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(false) }, - { "ExtendedSession", Value.ForBool(true) }, + { "IsExtendedSession", Value.ForBool(true) }, { "ExtendedSessionsIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); @@ -78,7 +78,7 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() orchestratorRequest.Properties.Clear(); orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(false) }, - { "ExtendedSession", Value.ForBool(true) }, + { "IsExtendedSession", Value.ForBool(true) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForString("hi") } }); requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); @@ -89,7 +89,7 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() orchestratorRequest.Properties.Clear(); orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(false) }, - { "ExtendedSession", Value.ForBool(true) } }); + { "IsExtendedSession", Value.ForBool(true) } }); requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); @@ -99,7 +99,7 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() orchestratorRequest.Properties.Clear(); orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(false) }, - { "extendedSession", Value.ForBool(true) }, + { "isExtendedSession", Value.ForBool(true) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); @@ -113,7 +113,7 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() orchestratorRequest.Properties.Clear(); orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(false) }, - { "ExtendedSession", Value.ForNumber(1) }, + { "IsExtendedSession", Value.ForNumber(1) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); @@ -165,7 +165,7 @@ public void MalformedPastEventsParameter_Means_NoHistoryRequired() // Misspelled include past events key orchestratorRequest.Properties.Add(new MapField() { { "INcludePastEvents", Value.ForBool(false) }, - { "ExtendedSession", Value.ForBool(false) }, + { "IsExtendedSession", Value.ForBool(false) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); @@ -177,7 +177,7 @@ public void MalformedPastEventsParameter_Means_NoHistoryRequired() orchestratorRequest.Properties.Clear(); orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForString("no") }, - { "ExtendedSession", Value.ForBool(false) }, + { "IsExtendedSession", Value.ForBool(false) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); @@ -188,7 +188,7 @@ public void MalformedPastEventsParameter_Means_NoHistoryRequired() // No include past events key orchestratorRequest.Properties.Clear(); orchestratorRequest.Properties.Add(new MapField() { - { "ExtendedSession", Value.ForBool(false) }, + { "IsExtendedSession", Value.ForBool(false) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); @@ -217,7 +217,7 @@ public void Incomplete_Orchestration_Stored() Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([historyEvent]); orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(true) }, - { "ExtendedSession", Value.ForBool(true) }, + { "IsExtendedSession", Value.ForBool(true) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); @@ -246,7 +246,7 @@ public void Complete_Orchestration_NotStored() Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([historyEvent]); orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(true) }, - { "ExtendedSession", Value.ForBool(true) }, + { "IsExtendedSession", Value.ForBool(true) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); @@ -275,7 +275,7 @@ public void ExternallyEndedExtendedSession_Evicted() Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([historyEvent]); orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(true) }, - { "ExtendedSession", Value.ForBool(true) }, + { "IsExtendedSession", Value.ForBool(true) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); @@ -287,7 +287,7 @@ public void ExternallyEndedExtendedSession_Evicted() orchestratorRequest.Properties.Clear(); orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(true) }, - { "ExtendedSession", Value.ForBool(false) }, + { "IsExtendedSession", Value.ForBool(false) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); @@ -317,7 +317,7 @@ public async void Stale_ExtendedSessions_Evicted_Async() Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([historyEvent]); orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(true) }, - { "ExtendedSession", Value.ForBool(true) }, + { "IsExtendedSession", Value.ForBool(true) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(extendedSessionIdleTimeout) } }); byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); @@ -333,7 +333,7 @@ public async void Stale_ExtendedSessions_Evicted_Async() orchestratorRequest.Properties.Clear(); orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(false) }, - { "ExtendedSession", Value.ForBool(true) }, + { "IsExtendedSession", Value.ForBool(true) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(extendedSessionIdleTimeout) } }); requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); @@ -363,7 +363,7 @@ public void PastEventIncludes_Means_ExtendedSession_Evicted() Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([historyEvent]); orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(true) }, - { "ExtendedSession", Value.ForBool(true) }, + { "IsExtendedSession", Value.ForBool(true) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(extendedSessionIdleTimeout) } }); byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); @@ -394,9 +394,12 @@ public void Null_ExtendedSessionsCache_IsOkay() } }; Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([historyEvent]); + + // Set up the parameters as if extended sessions are enabled, but do not pass an extended session cache to the request. + // The request should still be successful. orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(true) }, - { "ExtendedSession", Value.ForBool(true) }, + { "IsExtendedSession", Value.ForBool(true) }, { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); byte[] requestBytes = orchestratorRequest.ToByteArray(); string requestString = Convert.ToBase64String(requestBytes); From 6a0354a03298e03616822feb5e2afc4748b33d26 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 15 Sep 2025 18:39:09 -0700 Subject: [PATCH 38/40] slight change to avoid unnecessary initialization of the cache if properties are not specified, also added another test to make sure that extended sessions aren't stored if isExtendedSessions is false --- src/Worker/Grpc/GrpcOrchestrationRunner.cs | 14 +++--- .../GrpcOrchestrationRunnerTests.cs | 50 +++++++++++++------ 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index da9dce90d..aa44c615a 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -137,13 +137,18 @@ public static string LoadAndRun( bool addToExtendedSessions = false; bool requiresHistory = false; bool pastEventsIncluded = true; + bool isExtendedSession = false; double extendedSessionIdleTimeoutInSeconds = 0; + // Only attempt to initialize the extended sessions cache if all the parameters are correctly specified if (properties.TryGetValue("ExtendedSessionIdleTimeoutInSeconds", out object? extendedSessionIdleTimeoutObj) && extendedSessionIdleTimeoutObj is double extendedSessionIdleTimeout - && extendedSessionIdleTimeout >= 0) + && extendedSessionIdleTimeout >= 0 + && properties.TryGetValue("IsExtendedSession", out object? extendedSessionObj) + && extendedSessionObj is bool extendedSession) { extendedSessionIdleTimeoutInSeconds = extendedSessionIdleTimeout; + isExtendedSession = extendedSession; extendedSessions = extendedSessionsCache?.GetOrInitializeCache(extendedSessionIdleTimeoutInSeconds); } @@ -153,10 +158,7 @@ public static string LoadAndRun( pastEventsIncluded = includePastEvents; } - if (properties.TryGetValue("IsExtendedSession", out object? isExtendedSessionObj) - && isExtendedSessionObj is bool isExtendedSession - && isExtendedSession - && extendedSessions != null) + if (isExtendedSession && extendedSessions != null) { // If a history was provided, even if we already have an extended session stored, we always want to evict whatever state is in the cache and replace it with a new extended // session based on the provided history @@ -215,7 +217,7 @@ public static string LoadAndRun( if (addToExtendedSessions && !executor.IsCompleted) { - extendedSessions!.Set( + extendedSessions.Set( request.InstanceId, new(runtimeState, shim, executor), new MemoryCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(extendedSessionIdleTimeoutInSeconds) }); diff --git a/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs b/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs index 90c1a20d3..0e60087bb 100644 --- a/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs +++ b/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs @@ -58,7 +58,7 @@ public void EmptyHistory_Returns_NeedsHistoryInResponse() } [Fact] - public void MalformedRequestParameters_Means_NoExtendedSessionsStored() + public void MalformedRequestParameters_Means_CacheNotInitialized() { using var extendedSessions = new ExtendedSessionsCache(); Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([]); @@ -103,11 +103,8 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); - stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); - response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); - // The extended session is still initialized due to the well-formed extended session timeout key - Assert.True(extendedSessions.IsInitialized); - Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out object? extendedSession)); + GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + Assert.False(extendedSessions.IsInitialized); // Wrong value type for extended session key orchestratorRequest.Properties.Clear(); @@ -117,11 +114,8 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); - stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); - response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); - // The extended session is still initialized due to the well-formed extended session timeout key - Assert.True(extendedSessions.IsInitialized); - Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out extendedSession)); + GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + Assert.False(extendedSessions.IsInitialized); // No extended session key orchestratorRequest.Properties.Clear(); @@ -130,11 +124,25 @@ public void MalformedRequestParameters_Means_NoExtendedSessionsStored() { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); requestBytes = orchestratorRequest.ToByteArray(); requestString = Convert.ToBase64String(requestBytes); - stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); - response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); - // The extended session is still initialized due to the well-formed extended session timeout key + GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + Assert.False(extendedSessions.IsInitialized); + } + + [Fact] + public void IsExtendedSessionFalse_Means_NoExtendedSessionStored() + { + using var extendedSessions = new ExtendedSessionsCache(); + Protobuf.OrchestratorRequest orchestratorRequest = CreateOrchestratorRequest([]); + + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(false) }, + { "IsExtendedSession", Value.ForBool(false) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(DefaultExtendedSessionIdleTimeoutInSeconds) } }); + byte[] requestBytes = orchestratorRequest.ToByteArray(); + string requestString = Convert.ToBase64String(requestBytes); + GrpcOrchestrationRunner.LoadAndRun(requestString, new CallSubOrchestrationOrchestrator(), extendedSessions); Assert.True(extendedSessions.IsInitialized); - Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out extendedSession)); + Assert.False(extendedSessions.GetOrInitializeCache(DefaultExtendedSessionIdleTimeoutInSeconds).TryGetValue(TestInstanceId, out object? extendedSession)); } /// @@ -343,7 +351,7 @@ public async void Stale_ExtendedSessions_Evicted_Async() } [Fact] - public void PastEventIncludes_Means_ExtendedSession_Evicted() + public void PastEventIncluded_Means_ExtendedSession_Evicted() { using var extendedSessions = new ExtendedSessionsCache(); int extendedSessionIdleTimeout = 5; @@ -408,6 +416,16 @@ public void Null_ExtendedSessionsCache_IsOkay() Assert.Single(response.Actions); Assert.NotNull(response.Actions[0].CompleteOrchestration); Assert.Equal(Protobuf.OrchestrationStatus.Completed, response.Actions[0].CompleteOrchestration.OrchestrationStatus); + + // Now try it again without any properties specified. The request should still be successful. + orchestratorRequest.Properties.Clear(); + requestBytes = orchestratorRequest.ToByteArray(); + requestString = Convert.ToBase64String(requestBytes); + stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator()); + response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); + Assert.Single(response.Actions); + Assert.NotNull(response.Actions[0].CompleteOrchestration); + Assert.Equal(Protobuf.OrchestrationStatus.Completed, response.Actions[0].CompleteOrchestration.OrchestrationStatus); } static Protobuf.OrchestratorRequest CreateOrchestratorRequest(IEnumerable newEvents) From 11ab61d7896946be859736a7080136c4d25c5933 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 15 Sep 2025 19:39:24 -0700 Subject: [PATCH 39/40] slight change to make the idle timeout need to be >0 rather than >=0 --- src/Worker/Grpc/GrpcOrchestrationRunner.cs | 2 +- .../GrpcOrchestrationRunnerTests.cs | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Worker/Grpc/GrpcOrchestrationRunner.cs b/src/Worker/Grpc/GrpcOrchestrationRunner.cs index aa44c615a..74a92006a 100644 --- a/src/Worker/Grpc/GrpcOrchestrationRunner.cs +++ b/src/Worker/Grpc/GrpcOrchestrationRunner.cs @@ -143,7 +143,7 @@ public static string LoadAndRun( // Only attempt to initialize the extended sessions cache if all the parameters are correctly specified if (properties.TryGetValue("ExtendedSessionIdleTimeoutInSeconds", out object? extendedSessionIdleTimeoutObj) && extendedSessionIdleTimeoutObj is double extendedSessionIdleTimeout - && extendedSessionIdleTimeout >= 0 + && extendedSessionIdleTimeout > 0 && properties.TryGetValue("IsExtendedSession", out object? extendedSessionObj) && extendedSessionObj is bool extendedSession) { diff --git a/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs b/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs index 0e60087bb..6c3179211 100644 --- a/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs +++ b/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs @@ -85,6 +85,28 @@ public void MalformedRequestParameters_Means_CacheNotInitialized() GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); Assert.False(extendedSessions.IsInitialized); + // Invalid number for extended session timeout key (must be > 0) + orchestratorRequest.Properties.Clear(); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(false) }, + { "IsExtendedSession", Value.ForBool(true) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(0) } }); + requestBytes = orchestratorRequest.ToByteArray(); + requestString = Convert.ToBase64String(requestBytes); + GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + Assert.False(extendedSessions.IsInitialized); + + // Invalid number for extended session timeout key (must be > 0) + orchestratorRequest.Properties.Clear(); + orchestratorRequest.Properties.Add(new MapField() { + { "IncludePastEvents", Value.ForBool(false) }, + { "IsExtendedSession", Value.ForBool(true) }, + { "ExtendedSessionIdleTimeoutInSeconds", Value.ForNumber(-1) } }); + requestBytes = orchestratorRequest.ToByteArray(); + requestString = Convert.ToBase64String(requestBytes); + GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); + Assert.False(extendedSessions.IsInitialized); + // No extended session timeout key orchestratorRequest.Properties.Clear(); orchestratorRequest.Properties.Add(new MapField() { From f18a9105d9df0903c202ef29f4cdddaf736f382b Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Tue, 16 Sep 2025 23:02:44 -0700 Subject: [PATCH 40/40] updating version numbers and protos --- Directory.Packages.props | 2 +- eng/targets/Release.props | 2 +- src/Grpc/orchestrator_service.proto | 20 +++++++++++++++++++- src/Grpc/versions.txt | 4 ++-- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4beaa456f..34509fe65 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,7 +29,7 @@ - + diff --git a/eng/targets/Release.props b/eng/targets/Release.props index c731d3f9f..f4c376c63 100644 --- a/eng/targets/Release.props +++ b/eng/targets/Release.props @@ -17,7 +17,7 @@ - 1.14.0 + 1.15.0 diff --git a/src/Grpc/orchestrator_service.proto b/src/Grpc/orchestrator_service.proto index 3b9c4f408..df5143bc9 100644 --- a/src/Grpc/orchestrator_service.proto +++ b/src/Grpc/orchestrator_service.proto @@ -342,8 +342,10 @@ message OrchestratorResponse { // The number of work item events that were processed by the orchestrator. // This field is optional. If not set, the service should assume that the orchestrator processed all events. google.protobuf.Int32Value numEventsProcessed = 5; - OrchestrationTraceContext orchestrationTraceContext = 6; + + // Whether or not a history is required to complete the original OrchestratorRequest and none was provided. + bool requiresHistory = 7; } message CreateInstanceRequest { @@ -678,6 +680,18 @@ message AbandonEntityTaskResponse { // Empty. } +message SkipGracefulOrchestrationTerminationsRequest { + // A maximum of 500 instance IDs can be provided in this list. + repeated string instanceIds = 1; + google.protobuf.StringValue reason = 2; +} + +message SkipGracefulOrchestrationTerminationsResponse { + // Those instances which could not be terminated because they had locked entities at the time of this termination call, + // are already in a terminal state (completed, failed, terminated, etc.), are not orchestrations, or do not exist (i.e. have been purged) + repeated string unterminatedInstanceIds = 1; +} + service TaskHubSidecarService { // Sends a hello request to the sidecar service. rpc Hello(google.protobuf.Empty) returns (google.protobuf.Empty); @@ -751,6 +765,10 @@ service TaskHubSidecarService { // Abandon an entity work item rpc AbandonTaskEntityWorkItem(AbandonEntityTaskRequest) returns (AbandonEntityTaskResponse); + + // "Skip" graceful termination of orchestrations by immediately changing their status in storage to "terminated". + // Note that a maximum of 500 orchestrations can be terminated at a time using this method. + rpc SkipGracefulOrchestrationTerminations(SkipGracefulOrchestrationTerminationsRequest) returns (SkipGracefulOrchestrationTerminationsResponse); } message GetWorkItemsRequest { diff --git a/src/Grpc/versions.txt b/src/Grpc/versions.txt index e9f651378..3e4d1b210 100644 --- a/src/Grpc/versions.txt +++ b/src/Grpc/versions.txt @@ -1,2 +1,2 @@ -# The following files were downloaded from branch main at 2025-09-10 22:50:45 UTC -https://raw.githubusercontent.com/microsoft/durabletask-protobuf/985035a0890575ae18be0eb2a3ac93c10824498a/protos/orchestrator_service.proto +# The following files were downloaded from branch main at 2025-09-17 01:45:58 UTC +https://raw.githubusercontent.com/microsoft/durabletask-protobuf/f5745e0d83f608d77871c1894d9260ceaae08967/protos/orchestrator_service.proto