Skip to content

Commit e477b00

Browse files
bachuvcgillum
andauthored
Add target based scaling support for MSSQL (#169)
Co-authored-by: Chris Gillum <[email protected]>
1 parent 6003e41 commit e477b00

File tree

11 files changed

+181
-19
lines changed

11 files changed

+181
-19
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
# Changelog
22

3-
## v1.3.1 (Unreleased)
3+
## v1.4.0
4+
5+
### New
6+
7+
* Support for Azure Functions target-based scaling ([#169](https://github.com/microsoft/durabletask-mssql/pull/169))
8+
* Added `net6.0` TFM to Microsoft.DurableTask.SqlServer.AzureFunctions
49

510
### Updates
611

712
* Fix SQL retry logic to open a new connection if a previous failure closed the connection ([#221](https://github.com/microsoft/durabletask-mssql/pull/221)) - contributed by [@microrama](https://github.com/microrama)
13+
* Pin Microsoft.Azure.WebJobs.Extensions.DurableTask dependency to 2.13.7 instead of wildcard to avoid accidental build breaks
814

915
## v1.3.0
1016

src/DurableTask.SqlServer.AzureFunctions/DurableTask.SqlServer.AzureFunctions.csproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
<Import Project="../common.props" />
55

66
<PropertyGroup>
7-
<TargetFrameworks>netstandard2.0</TargetFrameworks>
7+
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
8+
</PropertyGroup>
9+
10+
<PropertyGroup Condition="'$(TargetFramework)' == 'net6.0'">
11+
<DefineConstants>$(DefineConstants);FUNCTIONS_V4</DefineConstants>
812
</PropertyGroup>
913

1014
<!-- NuGet package settings -->
@@ -16,7 +20,7 @@
1620
</PropertyGroup>
1721

1822
<ItemGroup>
19-
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="2.*" />
23+
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DurableTask" Version="2.13.7" />
2024
</ItemGroup>
2125

2226
<ItemGroup>

src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProvider.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ class SqlDurabilityProvider : DurabilityProvider
2323
readonly SqlOrchestrationService service;
2424

2525
SqlScaleMonitor? scaleMonitor;
26+
#if FUNCTIONS_V4
27+
SqlTargetScaler? targetScaler;
28+
#endif
2629

2730
public SqlDurabilityProvider(
2831
SqlOrchestrationService service,
@@ -197,8 +200,33 @@ public override bool TryGetScaleMonitor(
197200
string storageConnectionString,
198201
out IScaleMonitor scaleMonitor)
199202
{
200-
scaleMonitor = this.scaleMonitor ??= new SqlScaleMonitor(this.service, hubName);
203+
if (this.scaleMonitor == null)
204+
{
205+
var sqlMetricsProvider = new SqlMetricsProvider(this.service);
206+
this.scaleMonitor = new SqlScaleMonitor(hubName, sqlMetricsProvider);
207+
}
208+
209+
scaleMonitor = this.scaleMonitor;
210+
return true;
211+
}
212+
213+
#if FUNCTIONS_V4
214+
public override bool TryGetTargetScaler(
215+
string functionId,
216+
string functionName,
217+
string hubName,
218+
string connectionName,
219+
out ITargetScaler targetScaler)
220+
{
221+
if (this.targetScaler == null)
222+
{
223+
var sqlMetricsProvider = new SqlMetricsProvider(this.service);
224+
this.targetScaler = new SqlTargetScaler(hubName, sqlMetricsProvider);
225+
}
226+
227+
targetScaler = this.targetScaler;
201228
return true;
202229
}
230+
#endif
203231
}
204232
}

src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProviderFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public DurabilityProvider GetDurabilityProvider(DurableClientAttribute attribute
6666
lock (this.clientProviders)
6767
{
6868
string key = GetDurabilityProviderKey(attribute);
69-
if (this.clientProviders.TryGetValue(key, out DurabilityProvider clientProvider))
69+
if (this.clientProviders.TryGetValue(key, out DurabilityProvider? clientProvider))
7070
{
7171
return clientProvider;
7272
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace DurableTask.SqlServer.AzureFunctions
5+
{
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
public class SqlMetricsProvider
10+
{
11+
readonly SqlOrchestrationService service;
12+
13+
public SqlMetricsProvider(SqlOrchestrationService service)
14+
{
15+
this.service = service;
16+
}
17+
18+
public virtual async Task<SqlScaleMetric> GetMetricsAsync(int? previousWorkerCount = null)
19+
{
20+
// GetRecommendedReplicaCountAsync will write a trace if the recommendation results
21+
// in a worker count that is different from the worker count we pass in as an argument.
22+
int recommendedReplicaCount = await this.service.GetRecommendedReplicaCountAsync(
23+
previousWorkerCount,
24+
CancellationToken.None);
25+
26+
return new SqlScaleMetric { RecommendedReplicaCount = recommendedReplicaCount };
27+
}
28+
}
29+
}

src/DurableTask.SqlServer.AzureFunctions/SqlScaleMetric.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace DurableTask.SqlServer.AzureFunctions
55
{
66
using Microsoft.Azure.WebJobs.Host.Scale;
77

8-
class SqlScaleMetric : ScaleMetrics
8+
public class SqlScaleMetric : ScaleMetrics
99
{
1010
public int RecommendedReplicaCount { get; set; }
1111
}

src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,22 @@ class SqlScaleMonitor : IScaleMonitor<SqlScaleMetric>
1919
static readonly ScaleStatus NoScaleVote = new ScaleStatus { Vote = ScaleVote.None };
2020
static readonly ScaleStatus ScaleOutVote = new ScaleStatus { Vote = ScaleVote.ScaleOut };
2121

22-
readonly SqlOrchestrationService service;
22+
readonly SqlMetricsProvider metricsProvider;
2323

2424
int? previousWorkerCount = -1;
2525

26-
public SqlScaleMonitor(SqlOrchestrationService service, string taskHubName)
26+
public SqlScaleMonitor(string taskHubName, SqlMetricsProvider sqlMetricsProvider)
2727
{
28-
this.service = service ?? throw new ArgumentNullException(nameof(service));
29-
this.Descriptor = new ScaleMonitorDescriptor($"DurableTask-SqlServer:{taskHubName ?? "default"}");
28+
// Scalers in Durable Functions are shared for all functions in the same task hub.
29+
// So instead of using a function ID, we use the task hub name as the basis for the descriptor ID.
30+
string id = $"DurableTask-SqlServer:{taskHubName ?? "default"}";
31+
32+
#if FUNCTIONS_V4
33+
this.Descriptor = new ScaleMonitorDescriptor(id: id, functionId: id);
34+
#else
35+
this.Descriptor = new ScaleMonitorDescriptor(id);
36+
#endif
37+
this.metricsProvider = sqlMetricsProvider ?? throw new ArgumentNullException(nameof(sqlMetricsProvider));
3038
}
3139

3240
/// <inheritdoc />
@@ -38,13 +46,7 @@ public SqlScaleMonitor(SqlOrchestrationService service, string taskHubName)
3846
/// <inheritdoc />
3947
public async Task<SqlScaleMetric> GetMetricsAsync()
4048
{
41-
// GetRecommendedReplicaCountAsync will write a trace if the recommendation results
42-
// in a worker count that is different from the worker count we pass in as an argument.
43-
int recommendedReplicaCount = await this.service.GetRecommendedReplicaCountAsync(
44-
this.previousWorkerCount,
45-
CancellationToken.None);
46-
47-
return new SqlScaleMetric { RecommendedReplicaCount = recommendedReplicaCount };
49+
return await this.metricsProvider.GetMetricsAsync(this.previousWorkerCount);
4850
}
4951

5052
/// <inheritdoc />
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
#if FUNCTIONS_V4
5+
namespace DurableTask.SqlServer.AzureFunctions
6+
{
7+
using System;
8+
using System.Threading.Tasks;
9+
using Microsoft.Azure.WebJobs.Host.Scale;
10+
11+
public class SqlTargetScaler : ITargetScaler
12+
{
13+
readonly SqlMetricsProvider sqlMetricsProvider;
14+
15+
public SqlTargetScaler(string taskHubName, SqlMetricsProvider sqlMetricsProvider)
16+
{
17+
this.sqlMetricsProvider = sqlMetricsProvider;
18+
19+
// Scalers in Durable Functions are shared for all functions in the same task hub.
20+
// So instead of using a function ID, we use the task hub name as the basis for the descriptor ID.
21+
string id = $"DurableTask-SqlServer:{taskHubName ?? "default"}";
22+
this.TargetScalerDescriptor = new TargetScalerDescriptor(id);
23+
}
24+
25+
public TargetScalerDescriptor TargetScalerDescriptor { get; }
26+
27+
public async Task<TargetScalerResult> GetScaleResultAsync(TargetScalerContext context)
28+
{
29+
SqlScaleMetric sqlScaleMetric = await this.sqlMetricsProvider.GetMetricsAsync();
30+
return new TargetScalerResult
31+
{
32+
TargetWorkerCount = Math.Max(0, sqlScaleMetric.RecommendedReplicaCount),
33+
};
34+
}
35+
}
36+
}
37+
#endif

src/common.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<!-- Version settings: https://andrewlock.net/version-vs-versionsuffix-vs-packageversion-what-do-they-all-mean/ -->
1717
<PropertyGroup>
1818
<MajorVersion>1</MajorVersion>
19-
<MinorVersion>3</MinorVersion>
19+
<MinorVersion>4</MinorVersion>
2020
<PatchVersion>0</PatchVersion>
2121
<VersionPrefix>$(MajorVersion).$(MinorVersion).$(PatchVersion)</VersionPrefix>
2222
<VersionSuffix></VersionSuffix>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace DurableTask.SqlServer.AzureFunctions.Tests
5+
{
6+
using DurableTask.Core;
7+
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
8+
using Microsoft.Azure.WebJobs.Host.Scale;
9+
using Moq;
10+
using Xunit;
11+
12+
public class TargetBasedScalingTests
13+
{
14+
readonly Mock<SqlMetricsProvider> metricsProviderMock;
15+
readonly Mock<IOrchestrationService> orchestrationServiceMock;
16+
17+
public TargetBasedScalingTests()
18+
{
19+
this.orchestrationServiceMock = new Mock<IOrchestrationService>(MockBehavior.Strict);
20+
21+
SqlOrchestrationService? nullServiceArg = null; // not needed for this test
22+
this.metricsProviderMock = new Mock<SqlMetricsProvider>(
23+
behavior: MockBehavior.Strict,
24+
nullServiceArg);
25+
}
26+
27+
[Theory]
28+
[InlineData(0)]
29+
[InlineData(10)]
30+
[InlineData(20)]
31+
public async void TargetBasedScalingTest(int expectedTargetWorkerCount)
32+
{
33+
var durabilityProviderMock = new Mock<DurabilityProvider>(
34+
MockBehavior.Strict,
35+
"storageProviderName",
36+
this.orchestrationServiceMock.Object,
37+
new Mock<IOrchestrationServiceClient>().Object,
38+
"connectionName");
39+
40+
var sqlScaleMetric = new SqlScaleMetric()
41+
{
42+
RecommendedReplicaCount = expectedTargetWorkerCount,
43+
};
44+
45+
this.metricsProviderMock.Setup(m => m.GetMetricsAsync(null)).ReturnsAsync(sqlScaleMetric);
46+
47+
var targetScaler = new SqlTargetScaler(
48+
"functionId",
49+
this.metricsProviderMock.Object);
50+
51+
TargetScalerResult result = await targetScaler.GetScaleResultAsync(new TargetScalerContext());
52+
53+
Assert.Equal(expectedTargetWorkerCount, result.TargetWorkerCount);
54+
}
55+
}
56+
}

test/DurableTask.SqlServer.Tests/Integration/DatabaseManagement.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ async Task ValidateDatabaseSchemaAsync(TestDatabase database, string schemaName
503503
database.ConnectionString,
504504
schemaName);
505505
Assert.Equal(1, currentSchemaVersion.Major);
506-
Assert.Equal(3, currentSchemaVersion.Minor);
506+
Assert.Equal(4, currentSchemaVersion.Minor);
507507
Assert.Equal(0, currentSchemaVersion.Patch);
508508
}
509509

0 commit comments

Comments
 (0)