Skip to content

Commit cfb9685

Browse files
authored
Lightweight Scale Controller package for Azure Storage backend (#3357)
* initial commit * updte comments * udpate with comments and add new workflow * udpate coments * add nullable enable * udpate by comments * udpate by comments * fix comment from copilot
1 parent e986657 commit cfb9685

32 files changed

+2769
-1
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Functions Scale Tests Azure Storage
2+
permissions:
3+
contents: read
4+
5+
on:
6+
push:
7+
branches:
8+
- main
9+
- dev
10+
paths-ignore: [ '**.md' ]
11+
pull_request:
12+
branches:
13+
- '*'
14+
paths-ignore: [ '**.md' ]
15+
16+
env:
17+
solution: WebJobs.Extensions.DurableTask.sln
18+
config: Release
19+
AzureWebJobsStorage: UseDevelopmentStorage=true
20+
21+
jobs:
22+
build:
23+
runs-on: ubuntu-latest
24+
25+
steps:
26+
- uses: actions/checkout@v3
27+
with:
28+
submodules: true
29+
30+
- name: Setup .NET
31+
uses: actions/setup-dotnet@v3
32+
with:
33+
global-json-file: global.json
34+
35+
- name: Restore dependencies
36+
run: dotnet restore $solution
37+
38+
- name: Build
39+
run: dotnet build $solution -c $config
40+
41+
- name: Set up Node.js (needed for Azurite)
42+
uses: actions/setup-node@v3
43+
with:
44+
node-version: '18.x'
45+
46+
- name: Install Azurite
47+
run: npm install -g azurite
48+
49+
- name: Run Scale tests
50+
run : |
51+
azurite --silent --blobPort 10000 --queuePort 10001 --tablePort 10002 &
52+
AZURITE_PID=$!
53+
trap "kill $AZURITE_PID || true" EXIT
54+
55+
# Wait for Azurite to be ready
56+
for i in {1..30}; do
57+
if nc -z 127.0.0.1 10000; then
58+
echo "Azurite is ready"
59+
break
60+
fi
61+
echo "Azurite is not ready, waiting ($i/30)"
62+
sleep 1
63+
done
64+
65+
# Run tests
66+
dotnet test ./test/ScaleTests/Microsoft.Azure.WebJobs.Extensions.DurableTask.FunctionsScale.Tests.csproj -c $config --no-build --verbosity normal

Directory.Packages.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
<PackageVersion Include="Microsoft.AspNetCore.Mvc.WebApiCompatShim" Version="2.2.0" />
1616
<PackageVersion Include="Microsoft.Azure.DurableTask.ApplicationInsights" Version="0.9.0" />
1717
<PackageVersion Include="Microsoft.Azure.DurableTask.AzureStorage" Version="2.8.2" />
18+
<PackageVersion Include="Microsoft.Azure.DurableTask.Netherite" Version="3.1.1" />
19+
<PackageVersion Include="Microsoft.DurableTask.AzureManagedBackend" Version="1.3.0" />
20+
<PackageVersion Include="Microsoft.DurableTask.SqlServer" Version="1.5.2" />
1821
<PackageVersion Include="Microsoft.Azure.DurableTask.Core" Version="3.7.0" />
1922
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Core" Version="2.2.0" />
2023
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Abstractions" Version="1.3.0" />

WebJobs.Extensions.DurableTask.sln

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
Microsoft Visual Studio Solution File, Format Version 12.00
32
# Visual Studio Version 17
43
VisualStudioVersion = 17.0.31822.380
@@ -82,6 +81,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "E2E", "E2E", "{02EA681E-C7D
8281
EndProject
8382
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "app", "test\e2e\Apps\BasicDotNetIsolated\app.csproj", "{DBD5C544-70AC-5928-5DD1-C02918927439}"
8483
EndProject
84+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Azure.WebJobs.Extensions.DurableTask.FunctionsScale", "src\Microsoft.Azure.WebJobs.Extensions.DurableTask.FunctionsScale\Microsoft.Azure.WebJobs.Extensions.DurableTask.FunctionsScale.csproj", "{CDDAD51A-3516-401E-B0A4-7688E2787AB7}"
85+
EndProject
86+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Azure.WebJobs.Extensions.DurableTask.FunctionsScale.Tests", "test\ScaleTests\Microsoft.Azure.WebJobs.Extensions.DurableTask.FunctionsScale.Tests.csproj", "{F47A638F-634F-48CA-A1FC-83694327FBAE}"
87+
EndProject
8588
Global
8689
GlobalSection(SolutionConfigurationPlatforms) = preSolution
8790
Debug|Any CPU = Debug|Any CPU
@@ -141,6 +144,14 @@ Global
141144
{DBD5C544-70AC-5928-5DD1-C02918927439}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
142145
{DBD5C544-70AC-5928-5DD1-C02918927439}.Release|Any CPU.ActiveCfg = Release|Any CPU
143146
{DBD5C544-70AC-5928-5DD1-C02918927439}.Release|Any CPU.Build.0 = Release|Any CPU
147+
{CDDAD51A-3516-401E-B0A4-7688E2787AB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
148+
{CDDAD51A-3516-401E-B0A4-7688E2787AB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
149+
{CDDAD51A-3516-401E-B0A4-7688E2787AB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
150+
{CDDAD51A-3516-401E-B0A4-7688E2787AB7}.Release|Any CPU.Build.0 = Release|Any CPU
151+
{F47A638F-634F-48CA-A1FC-83694327FBAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
152+
{F47A638F-634F-48CA-A1FC-83694327FBAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
153+
{F47A638F-634F-48CA-A1FC-83694327FBAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
154+
{F47A638F-634F-48CA-A1FC-83694327FBAE}.Release|Any CPU.Build.0 = Release|Any CPU
144155
EndGlobalSection
145156
GlobalSection(SolutionProperties) = preSolution
146157
HideSolutionNode = FALSE
@@ -168,6 +179,8 @@ Global
168179
{63628712-4196-4865-B268-5BA3D8F08DE1} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
169180
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {78BCF152-C22C-408F-9FB1-0F8C99B154B5}
170181
{DBD5C544-70AC-5928-5DD1-C02918927439} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
182+
{CDDAD51A-3516-401E-B0A4-7688E2787AB7} = {7EC858EE-3481-4A82-AED4-CB00C34F42D0}
183+
{F47A638F-634F-48CA-A1FC-83694327FBAE} = {78BCF152-C22C-408F-9FB1-0F8C99B154B5}
171184
EndGlobalSection
172185
GlobalSection(ExtensibilityGlobals) = postSolution
173186
SolutionGuid = {5E9AC327-DE18-41A5-A55D-E44CB4281943}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using System.Runtime.CompilerServices;
5+
6+
[assembly: InternalsVisibleTo("Microsoft.Azure.WebJobs.Extensions.DurableTask.FunctionsScale.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100cd1dabd5a893b40e75dc901fe7293db4a3caf9cd4d3e3ed6178d49cd476969abe74a9e0b7f4a0bb15edca48758155d35a4f05e6e852fff1b319d103b39ba04acbadd278c2753627c95e1f6f6582425374b92f51cca3deb0d2aab9de3ecda7753900a31f70a236f163006beefffe282888f85e3c76d1205ec7dfef7fa472a17b1")]
7+
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using System;
5+
using DurableTask.AzureStorage;
6+
using Microsoft.Azure.WebJobs.Host.Scale;
7+
using Microsoft.Extensions.Logging;
8+
9+
namespace Microsoft.Azure.WebJobs.Extensions.DurableTask.FunctionsScale.AzureStorage
10+
{
11+
/// <summary>
12+
/// Azure Storage backend implementation of the scalability provider for Durable Functions scaling decisions.
13+
/// </summary>
14+
public class AzureStorageScalabilityProvider : ScalabilityProvider
15+
{
16+
private readonly StorageAccountClientProvider storageAccountClientProvider;
17+
private readonly string connectionName;
18+
private readonly ILogger logger;
19+
20+
private readonly object initLock = new object();
21+
22+
private DurableTaskMetricsProvider singletonDurableTaskMetricsProvider;
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="AzureStorageScalabilityProvider"/> class.
26+
/// </summary>
27+
/// <param name="storageAccountClientProvider">
28+
/// Provides Azure Storage clients using resolved configuration, including
29+
/// connection strings or token-based credentials.</param>
30+
/// <param name="connectionName">The name of the storage connection used to resolve host configuration.</param>
31+
/// <param name="logger">The logger instance used for diagnostics and telemetry.</param>
32+
public AzureStorageScalabilityProvider(
33+
StorageAccountClientProvider storageAccountClientProvider,
34+
string connectionName,
35+
ILogger logger)
36+
: base("AzureStorage", connectionName)
37+
{
38+
this.storageAccountClientProvider = storageAccountClientProvider ?? throw new ArgumentNullException(nameof(storageAccountClientProvider));
39+
this.connectionName = connectionName;
40+
this.logger = logger;
41+
}
42+
43+
/// <summary>
44+
/// Gets the app setting containing the Azure Storage connection string.
45+
/// </summary>
46+
public override string ConnectionName => this.connectionName;
47+
48+
/// <inheritdoc/>
49+
/// Note: ScaleMonitor is not used in prod. Can be cleaned in future.
50+
public override bool TryGetScaleMonitor(
51+
string functionId,
52+
string functionName,
53+
string hubName,
54+
string targetConnectionName,
55+
out IScaleMonitor scaleMonitor)
56+
{
57+
lock (this.initLock)
58+
{
59+
if (this.singletonDurableTaskMetricsProvider == null)
60+
{
61+
this.singletonDurableTaskMetricsProvider = this.GetMetricsProvider(
62+
hubName,
63+
this.storageAccountClientProvider,
64+
this.logger);
65+
}
66+
67+
scaleMonitor = new DurableTaskScaleMonitor(functionId, hubName, this.logger, this.singletonDurableTaskMetricsProvider);
68+
return true;
69+
}
70+
}
71+
72+
/// <inheritdoc/>
73+
public override bool TryGetTargetScaler(
74+
string functionId,
75+
string functionName,
76+
string hubName,
77+
string targetConnectionName,
78+
out ITargetScaler targetScaler)
79+
{
80+
lock (this.initLock)
81+
{
82+
if (this.singletonDurableTaskMetricsProvider == null)
83+
{
84+
this.singletonDurableTaskMetricsProvider = this.GetMetricsProvider(
85+
hubName,
86+
this.storageAccountClientProvider,
87+
this.logger);
88+
}
89+
90+
targetScaler = new DurableTaskTargetScaler(functionId, this.singletonDurableTaskMetricsProvider, this, this.logger);
91+
return true;
92+
}
93+
}
94+
95+
internal DurableTaskMetricsProvider GetMetricsProvider(
96+
string hubName,
97+
StorageAccountClientProvider clientProvider,
98+
ILogger metricsLogger)
99+
{
100+
return new DurableTaskMetricsProvider(hubName, metricsLogger, performanceMonitor: null, clientProvider);
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)