Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
dfb12ca
as light scale
nytian Oct 29, 2025
0b2b2c6
update azurestorage and update sql
nytian Nov 3, 2025
335fff8
add azuremanaged
nytian Nov 5, 2025
2c51fb7
update
nytian Nov 5, 2025
a869d52
Merge branch 'dev' into nytian/sc-light-pkg
nytian Nov 5, 2025
9867a99
update
nytian Nov 5, 2025
af5545f
udpate options config to use metadata instead of di options
nytian Nov 5, 2025
f15aea3
Merge branch 'nytian/sc-light-pkg' of https://github.com/Azure/azure-…
nytian Nov 5, 2025
25e49ce
remove di options and use triggeremetada and update tests accordingly
nytian Nov 10, 2025
5973886
add scale test ci yml
nytian Nov 10, 2025
ffd44fd
Merge branch 'dev' into nytian/sc-light-pkg
nytian Nov 10, 2025
a2956b4
add nullable check to remove all build warnings
nytian Nov 10, 2025
0e950a0
udpate test
nytian Nov 10, 2025
a8e9bef
Merge branch 'nytian/sc-light-pkg' of https://github.com/Azure/azure-…
nytian Nov 10, 2025
9d8dd48
udpate yml
nytian Nov 10, 2025
9e52edf
udpate test
nytian Nov 10, 2025
8188cbe
udpate yml
nytian Nov 11, 2025
d9fbed3
udpate test
nytian Nov 11, 2025
9692154
update test
nytian Nov 11, 2025
2dd1e66
update hub name
nytian Nov 11, 2025
93c15b7
Merge branch 'dev' into nytian/sc-light-pkg
nytian Nov 13, 2025
f94ff06
update
nytian Nov 17, 2025
54ff4c2
Merge branch 'dev' into nytian/sc-light-pkg
nytian Nov 17, 2025
709487b
fix const string
nytian Nov 17, 2025
9ed6630
Merge branch 'nytian/sc-light-pkg' of https://github.com/Azure/azure-…
nytian Nov 17, 2025
fbb2acc
fix warning
nytian Nov 17, 2025
ebfefca
debug failed test
nytian Nov 17, 2025
ed56b50
update test
nytian Nov 17, 2025
09e3ece
update test
nytian Nov 17, 2025
ec4f7a1
update test
nytian Nov 17, 2025
80d13f4
fix
nytian Nov 18, 2025
bfb4207
update
nytian Nov 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using DurableTask.AzureStorage;
using Microsoft.Azure.WebJobs.Host.Scale;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;

namespace Microsoft.Azure.WebJobs.Extensions.DurableTask.Scale.AzureStorage
{
/// <summary>
/// The Azure Storage implementation of additional methods not required by IOrchestrationService.
/// </summary>
public class AzureStorageDurabilityProvider : DurabilityProvider
{
private readonly AzureStorageOrchestrationService serviceClient;
private readonly IStorageServiceClientProviderFactory clientProviderFactory;
private readonly string connectionName;
private readonly JObject storageOptionsJson;
private readonly ILogger logger;

private readonly object initLock = new object();

private DurableTaskMetricsProvider singletonDurableTaskMetricsProvider;

public AzureStorageDurabilityProvider(
AzureStorageOrchestrationService service,
IStorageServiceClientProviderFactory clientProviderFactory,
string connectionName,
AzureStorageOptions options,
ILogger logger)
: base("Azure Storage", service, service, connectionName)
{
this.serviceClient = service;
this.clientProviderFactory = clientProviderFactory;
this.connectionName = connectionName;
this.storageOptionsJson = JObject.FromObject(
options,
new JsonSerializer
{
Converters = { new StringEnumConverter() },
ContractResolver = new CamelCasePropertyNamesContractResolver(),
});
this.logger = logger;
}

/// <summary>
/// The app setting containing the Azure Storage connection string.
/// </summary>
public override string ConnectionName => this.connectionName;

public override JObject ConfigurationJson => this.storageOptionsJson;

public override string EventSourceName { get; set; } = "DurableTask-AzureStorage";

internal DurableTaskMetricsProvider GetMetricsProvider(
string hubName,
StorageAccountClientProvider storageAccountClientProvider,
ILogger logger)
{
return new DurableTaskMetricsProvider(hubName, logger, performanceMonitor: null, storageAccountClientProvider);
}

/// <inheritdoc/>
public override bool TryGetScaleMonitor(
string functionId,
string functionName,
string hubName,
string connectionName,
out IScaleMonitor scaleMonitor)
{
lock (this.initLock)
{
if (this.singletonDurableTaskMetricsProvider == null)
{
// This is only called by the ScaleController, it doesn't run in the Functions Host process.
this.singletonDurableTaskMetricsProvider = this.GetMetricsProvider(
hubName,
this.clientProviderFactory.GetClientProvider(connectionName),
this.logger);
}

scaleMonitor = new DurableTaskScaleMonitor(functionId, hubName, this.logger, this.singletonDurableTaskMetricsProvider);
return true;
}
}

public override bool TryGetTargetScaler(
string functionId,
string functionName,
string hubName,
string connectionName,
out ITargetScaler targetScaler)
{
lock (this.initLock)
{
if (this.singletonDurableTaskMetricsProvider == null)
{
// This is only called by the ScaleController, it doesn't run in the Functions Host process.
this.singletonDurableTaskMetricsProvider = this.GetMetricsProvider(
hubName,
this.clientProviderFactory.GetClientProvider(connectionName),
this.logger);
}

targetScaler = new DurableTaskTargetScaler(functionId, this.singletonDurableTaskMetricsProvider, this, this.logger);
return true;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System;
using System.Text.Json;
using DurableTask.AzureStorage;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.Azure.WebJobs.Extensions.DurableTask.Scale.AzureStorage
{
public class AzureStorageDurabilityProviderFactory : IDurabilityProviderFactory
{
private const string LoggerName = "Host.Triggers.DurableTask.AzureStorage";
internal const string ProviderName = "AzureStorage";

private readonly DurableTaskOptions options;
private readonly IStorageServiceClientProviderFactory clientProviderFactory;
private readonly AzureStorageOptions azureStorageOptions;
private readonly INameResolver nameResolver;
private readonly ILoggerFactory loggerFactory;
private readonly bool useSeparateQueueForEntityWorkItems;
private readonly bool inConsumption; // If true, optimize defaults for consumption
private AzureStorageDurabilityProvider defaultStorageProvider;

// Must wait to get settings until we have validated taskhub name.
private bool hasValidatedOptions;
private AzureStorageOrchestrationServiceSettings defaultSettings;

/// <summary>
///
/// </summary>
/// <param name="options"></param>
/// <param name="clientProviderFactory"></param>
/// <param name="nameResolver"></param>
/// <param name="loggerFactory"></param>
/// <param name="platformInfo"></param>
/// <exception cref="ArgumentNullException"></exception>
public AzureStorageDurabilityProviderFactory(
IOptions<DurableTaskOptions> options,
IStorageServiceClientProviderFactory clientProviderFactory,
INameResolver nameResolver,
ILoggerFactory loggerFactory,
#pragma warning disable CS0612 // Type or member is obsolete
IPlatformInformation platformInfo)
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

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

Missing type definition: IPlatformInformation is referenced but not defined in the provided files. Ensure this interface exists in the codebase.

Copilot uses AI. Check for mistakes.
#pragma warning restore CS0612 // Type or member is obsolete
{
// this constructor may be called by dependency injection even if the AzureStorage provider is not selected
// in that case, return immediately, since this provider is not actually used, but can still throw validation errors
if (options.Value.StorageProvider.TryGetValue("type", out object value)
&& value is string s
&& !string.Equals(s, this.Name, StringComparison.OrdinalIgnoreCase))
{
return;
}

this.options = options.Value;
this.clientProviderFactory = clientProviderFactory ?? throw new ArgumentNullException(nameof(clientProviderFactory));
this.nameResolver = nameResolver ?? throw new ArgumentNullException(nameof(nameResolver));
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));

this.azureStorageOptions = new AzureStorageOptions();
this.inConsumption = platformInfo.IsInConsumptionPlan();

// The consumption plan has different performance characteristics so we provide
// different defaults for key configuration values.
int maxConcurrentOrchestratorsDefault = this.inConsumption ? 5 : 10 * Environment.ProcessorCount;
int maxConcurrentActivitiesDefault = this.inConsumption ? 10 : 10 * Environment.ProcessorCount;
int maxConcurrentEntitiesDefault = this.inConsumption ? 10 : 10 * Environment.ProcessorCount;
int maxEntityOperationBatchSizeDefault = this.inConsumption ? 50 : 5000;

if (this.inConsumption)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I wonder if we should keep the logic from line 72 to line 92. Cause when I checked the TriggerMetadata json payload from SC I didn't find info about runtime. Also I think this basically is not related with scaling?

{
WorkerRuntimeType language = platformInfo.GetWorkerRuntimeType();
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

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

Missing type definition: WorkerRuntimeType is referenced but not defined in the provided files. Ensure this enum exists in the codebase.

Copilot uses AI. Check for mistakes.
if (language == WorkerRuntimeType.Python)
{
this.azureStorageOptions.ControlQueueBufferThreshold = 32;
}
else
{
this.azureStorageOptions.ControlQueueBufferThreshold = 128;
}
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

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

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Suggested change
if (language == WorkerRuntimeType.Python)
{
this.azureStorageOptions.ControlQueueBufferThreshold = 32;
}
else
{
this.azureStorageOptions.ControlQueueBufferThreshold = 128;
}
this.azureStorageOptions.ControlQueueBufferThreshold = (language == WorkerRuntimeType.Python) ? 32 : 128;

Copilot uses AI. Check for mistakes.
}

WorkerRuntimeType runtimeType = platformInfo.GetWorkerRuntimeType();
if (runtimeType == WorkerRuntimeType.DotNetIsolated ||
runtimeType == WorkerRuntimeType.Java ||
runtimeType == WorkerRuntimeType.Custom)
{
this.useSeparateQueueForEntityWorkItems = true;
}

// The following defaults are only applied if the customer did not explicitely set them on `host.json`
this.options.MaxConcurrentOrchestratorFunctions = this.options.MaxConcurrentOrchestratorFunctions ?? maxConcurrentOrchestratorsDefault;
this.options.MaxConcurrentActivityFunctions = this.options.MaxConcurrentActivityFunctions ?? maxConcurrentActivitiesDefault;
this.options.MaxConcurrentEntityFunctions = this.options.MaxConcurrentEntityFunctions ?? maxConcurrentEntitiesDefault;
this.options.MaxEntityOperationBatchSize = this.options.MaxEntityOperationBatchSize ?? maxEntityOperationBatchSizeDefault;

// Override the configuration defaults with user-provided values in host.json, if any.
if (this.options.StorageProvider != null)
{
var json = JsonSerializer.Serialize(this.options.StorageProvider);
var newOptions = JsonSerializer.Deserialize<AzureStorageOptions>(json);
if (newOptions != null)
{
// Copy deserialized values into the existing options object
foreach (var prop in typeof(AzureStorageOptions).GetProperties())
{
var value = prop.GetValue(newOptions);
if (value != null)
{
prop.SetValue(this.azureStorageOptions, value);
}
}
}
}

var logger = loggerFactory.CreateLogger(nameof(this.azureStorageOptions));
this.azureStorageOptions.Validate(logger);

this.DefaultConnectionName = this.azureStorageOptions.ConnectionName ?? ConnectionStringNames.Storage;
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

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

Missing type definition: ConnectionStringNames is referenced but not defined in the provided files. Ensure this type exists or replace with a string constant.

Suggested change
this.DefaultConnectionName = this.azureStorageOptions.ConnectionName ?? ConnectionStringNames.Storage;
this.DefaultConnectionName = this.azureStorageOptions.ConnectionName ?? "AzureWebJobsStorage";

Copilot uses AI. Check for mistakes.
}

public virtual string Name => ProviderName;

public string DefaultConnectionName { get; }

// This method should not be called before the app settings are resolved into the options.
// Because of this, we wait to validate the options until right before building a durability provider, rather
// than in the Factory constructor.
private void EnsureDefaultClientSettingsInitialized()
{
if (!this.hasValidatedOptions)
{
if (!this.options.IsDefaultHubName())
{
this.azureStorageOptions.ValidateHubName(this.options.HubName);
}
else if (!this.azureStorageOptions.IsSanitizedHubName(this.options.HubName, out string sanitizedHubName))
{
this.options.SetDefaultHubName(sanitizedHubName);
}

this.defaultSettings = this.GetAzureStorageOrchestrationServiceSettings();
this.hasValidatedOptions = true;
}
}

public virtual DurabilityProvider GetDurabilityProvider()
{
this.EnsureDefaultClientSettingsInitialized();
if (this.defaultStorageProvider == null)
{
var defaultService = new AzureStorageOrchestrationService(this.defaultSettings);
ILogger logger = this.loggerFactory.CreateLogger(LoggerName);
this.defaultStorageProvider = new AzureStorageDurabilityProvider(
defaultService,
this.clientProviderFactory,
this.DefaultConnectionName,
this.azureStorageOptions,
logger);
}

return this.defaultStorageProvider;
}

public virtual DurabilityProvider GetDurabilityProvider(DurableClientAttribute attribute)
{
if (!attribute.ExternalClient)
{
this.EnsureDefaultClientSettingsInitialized();
}

return this.GetAzureStorageStorageProvider(attribute);
}

private AzureStorageDurabilityProvider GetAzureStorageStorageProvider(DurableClientAttribute attribute)
{
string connectionName = attribute.ConnectionName ?? this.DefaultConnectionName;
AzureStorageOrchestrationServiceSettings settings = this.GetAzureStorageOrchestrationServiceSettings(connectionName, attribute.TaskHub);

AzureStorageDurabilityProvider innerClient;

// Need to check this.defaultStorageProvider != null for external clients that call GetDurabilityProvider(attribute)
// which never initializes the defaultStorageProvider.
if (string.Equals(this.DefaultConnectionName, connectionName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(this.options.HubName, settings.TaskHubName, StringComparison.OrdinalIgnoreCase) &&
this.defaultStorageProvider != null)
{
// It's important that clients use the same AzureStorageOrchestrationService instance
// as the host when possible to ensure we any send operations can be picked up
// immediately instead of waiting for the next queue polling interval.
innerClient = this.defaultStorageProvider;
}
else
{
ILogger logger = this.loggerFactory.CreateLogger(LoggerName);
innerClient = new AzureStorageDurabilityProvider(
new AzureStorageOrchestrationService(settings),
this.clientProviderFactory,
connectionName,
this.azureStorageOptions,
logger);
}

return innerClient;
}

internal AzureStorageOrchestrationServiceSettings GetAzureStorageOrchestrationServiceSettings(
string connectionName = null,
string taskHubNameOverride = null)
{

var settings = new AzureStorageOrchestrationServiceSettings
{
StorageAccountClientProvider = this.clientProviderFactory.GetClientProvider(connectionName ?? this.DefaultConnectionName),
TaskHubName = taskHubNameOverride ?? this.options.HubName,
PartitionCount = this.azureStorageOptions.PartitionCount,
ControlQueueBatchSize = this.azureStorageOptions.ControlQueueBatchSize,
ControlQueueBufferThreshold = this.azureStorageOptions.ControlQueueBufferThreshold,
ControlQueueVisibilityTimeout = this.azureStorageOptions.ControlQueueVisibilityTimeout,
WorkItemQueueVisibilityTimeout = this.azureStorageOptions.WorkItemQueueVisibilityTimeout,
MaxConcurrentTaskOrchestrationWorkItems = this.options.MaxConcurrentOrchestratorFunctions ?? throw new InvalidOperationException($"{nameof(this.options.MaxConcurrentOrchestratorFunctions)} needs a default value"),
MaxConcurrentTaskActivityWorkItems = this.options.MaxConcurrentActivityFunctions ?? throw new InvalidOperationException($"{nameof(this.options.MaxConcurrentOrchestratorFunctions)} needs a default value"),
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

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

Incorrect property name in error message: The error message references 'MaxConcurrentOrchestratorFunctions' but should reference 'MaxConcurrentActivityFunctions' to match the property being validated.

Suggested change
MaxConcurrentTaskActivityWorkItems = this.options.MaxConcurrentActivityFunctions ?? throw new InvalidOperationException($"{nameof(this.options.MaxConcurrentOrchestratorFunctions)} needs a default value"),
MaxConcurrentTaskActivityWorkItems = this.options.MaxConcurrentActivityFunctions ?? throw new InvalidOperationException($"{nameof(this.options.MaxConcurrentActivityFunctions)} needs a default value"),

Copilot uses AI. Check for mistakes.
MaxConcurrentTaskEntityWorkItems = this.options.MaxConcurrentEntityFunctions ?? throw new InvalidOperationException($"{nameof(this.options.MaxConcurrentEntityFunctions)} needs a default value"),
ExtendedSessionsEnabled = this.options.ExtendedSessionsEnabled,
ExtendedSessionIdleTimeout = extendedSessionTimeout,
MaxQueuePollingInterval = this.azureStorageOptions.MaxQueuePollingInterval,
TrackingServiceClientProvider = this.azureStorageOptions.TrackingStoreConnectionName != null
? this.clientProviderFactory.GetTrackingClientProvider(this.azureStorageOptions.TrackingStoreConnectionName)
: null,
FetchLargeMessageDataEnabled = this.azureStorageOptions.FetchLargeMessagesAutomatically,
ThrowExceptionOnInvalidDedupeStatus = true,
UseAppLease = this.options.UseAppLease,
AppLeaseOptions = this.options.AppLeaseOptions,
AppName = EndToEndTraceHelper.LocalAppName,
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

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

Missing type definition: EndToEndTraceHelper is referenced but not defined in the provided files. Ensure this type exists or replace with an appropriate alternative.

Suggested change
AppName = EndToEndTraceHelper.LocalAppName,
AppName = AppDomain.CurrentDomain.FriendlyName,

Copilot uses AI. Check for mistakes.
LoggerFactory = this.loggerFactory,
UseLegacyPartitionManagement = this.azureStorageOptions.UseLegacyPartitionManagement,
UseTablePartitionManagement = this.azureStorageOptions.UseTablePartitionManagement,
UseSeparateQueueForEntityWorkItems = this.useSeparateQueueForEntityWorkItems,
EntityMessageReorderWindowInMinutes = this.options.EntityMessageReorderWindowInMinutes,
MaxEntityOperationBatchSize = this.options.MaxEntityOperationBatchSize,
AllowReplayingTerminalInstances = this.azureStorageOptions.AllowReplayingTerminalInstances,
PartitionTableOperationTimeout = this.azureStorageOptions.PartitionTableOperationTimeout,
QueueClientMessageEncoding = this.azureStorageOptions.QueueClientMessageEncoding,
};

if (this.inConsumption)
{
settings.MaxStorageOperationConcurrency = 25;
}

// When running on App Service VMSS stamps, these environment variables are the best way
// to enure unqique worker names
Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

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

Corrected spelling of 'enure' to 'ensure' and 'unqique' to 'unique'.

Suggested change
// to enure unqique worker names
// to ensure unique worker names

Copilot uses AI. Check for mistakes.
string stamp = this.nameResolver.Resolve("WEBSITE_CURRENT_STAMPNAME");
string roleInstance = this.nameResolver.Resolve("RoleInstanceId");
if (!string.IsNullOrEmpty(stamp) && !string.IsNullOrEmpty(roleInstance))
{
settings.WorkerId = $"{stamp}:{roleInstance}";
}

if (!string.IsNullOrEmpty(this.azureStorageOptions.TrackingStoreNamePrefix))
{
settings.TrackingStoreNamePrefix = this.azureStorageOptions.TrackingStoreNamePrefix;
}

return settings;
}
}
}
Loading
Loading