From fae10cabc77ac6ffc85a3ead7730e84a9647029c Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Thu, 4 May 2023 15:46:14 -0700 Subject: [PATCH 1/7] initial commit --- ...urableTask.SqlServer.AzureFunctions.csproj | 2 +- .../SqlDurabilityProvider.cs | 18 ++++++++++- .../SqlMetricsProvider.cs | 31 ++++++++++++++++++ .../SqlScaleMonitor.cs | 12 +++---- .../SqlTargetScaler.cs | 32 +++++++++++++++++++ 5 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs create mode 100644 src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs diff --git a/src/DurableTask.SqlServer.AzureFunctions/DurableTask.SqlServer.AzureFunctions.csproj b/src/DurableTask.SqlServer.AzureFunctions/DurableTask.SqlServer.AzureFunctions.csproj index 848bcc3..59f8f36 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/DurableTask.SqlServer.AzureFunctions.csproj +++ b/src/DurableTask.SqlServer.AzureFunctions/DurableTask.SqlServer.AzureFunctions.csproj @@ -4,7 +4,7 @@ - netstandard2.0 + netstandard2.1;netcoreapp3.1 diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProvider.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProvider.cs index 3cda4fe..b7f85db 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProvider.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProvider.cs @@ -197,8 +197,24 @@ public override bool TryGetScaleMonitor( string storageConnectionString, out IScaleMonitor scaleMonitor) { - scaleMonitor = this.scaleMonitor ??= new SqlScaleMonitor(this.service, hubName); + SqlMetricsProvider sqlMetricsProvider = new SqlMetricsProvider(this.service); + scaleMonitor = this.scaleMonitor ??= new SqlScaleMonitor(this.service, hubName, sqlMetricsProvider); return true; } + +#if NETCOREAPP + public override bool TryGetTargetScaler( + string functionId, + string functionName, + string hubName, + string connectionName, + out ITargetScaler targetScaler) + { + SqlMetricsProvider sqlMetricsProvider = new SqlMetricsProvider(this.service); + targetScaler = new SqlTargetScaler(functionId, sqlMetricsProvider); + + return true; + } +#endif } } diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs new file mode 100644 index 0000000..b78b03d --- /dev/null +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace DurableTask.SqlServer.AzureFunctions +{ + using System.Threading; + using System.Threading.Tasks; + + class SqlMetricsProvider + { + readonly SqlOrchestrationService service; + readonly int? previousWorkerCount; + + public SqlMetricsProvider(SqlOrchestrationService service, int? previousWorkerCount = null) + { + this.service = service; + this.previousWorkerCount = previousWorkerCount; + } + + public async Task GetMetricsAsync() + { + // GetRecommendedReplicaCountAsync will write a trace if the recommendation results + // in a worker count that is different from the worker count we pass in as an argument. + int recommendedReplicaCount = await this.service.GetRecommendedReplicaCountAsync( + this.previousWorkerCount, + CancellationToken.None); + + return new SqlScaleMetric { RecommendedReplicaCount = recommendedReplicaCount }; + } + } +} diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs index 447dc3e..b60870d 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs @@ -20,13 +20,15 @@ class SqlScaleMonitor : IScaleMonitor static readonly ScaleStatus ScaleOutVote = new ScaleStatus { Vote = ScaleVote.ScaleOut }; readonly SqlOrchestrationService service; + readonly SqlMetricsProvider metricsProvider; int? previousWorkerCount = -1; - public SqlScaleMonitor(SqlOrchestrationService service, string taskHubName) + public SqlScaleMonitor(SqlOrchestrationService service, string taskHubName, SqlMetricsProvider sqlMetricsProvider) { this.service = service ?? throw new ArgumentNullException(nameof(service)); this.Descriptor = new ScaleMonitorDescriptor($"DurableTask-SqlServer:{taskHubName ?? "default"}"); + this.metricsProvider = sqlMetricsProvider ?? throw new ArgumentNullException(nameof(sqlMetricsProvider)); } /// @@ -38,13 +40,7 @@ public SqlScaleMonitor(SqlOrchestrationService service, string taskHubName) /// public async Task GetMetricsAsync() { - // GetRecommendedReplicaCountAsync will write a trace if the recommendation results - // in a worker count that is different from the worker count we pass in as an argument. - int recommendedReplicaCount = await this.service.GetRecommendedReplicaCountAsync( - this.previousWorkerCount, - CancellationToken.None); - - return new SqlScaleMetric { RecommendedReplicaCount = recommendedReplicaCount }; + return await this.metricsProvider.GetMetricsAsync(); } /// diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs new file mode 100644 index 0000000..444e615 --- /dev/null +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace DurableTask.SqlServer.AzureFunctions +{ + using System.Threading.Tasks; + using Microsoft.Azure.WebJobs.Host.Scale; + +#if NETCOREAPP + class SqlTargetScaler : ITargetScaler + { + readonly SqlMetricsProvider sqlMetricsProvider; + readonly TargetScalerResult scaleResult; + + public SqlTargetScaler(string functionId, SqlMetricsProvider sqlMetricsProvider) + { + this.sqlMetricsProvider = sqlMetricsProvider; + this.scaleResult = new TargetScalerResult(); + this.TargetScalerDescriptor = new TargetScalerDescriptor(functionId); + } + + public TargetScalerDescriptor TargetScalerDescriptor { get; private set; } + + public async Task GetScaleResultAsync(TargetScalerContext context) + { + SqlScaleMetric sqlScaleMetric = await this.sqlMetricsProvider.GetMetricsAsync(); + this.scaleResult.TargetWorkerCount = sqlScaleMetric.RecommendedReplicaCount; + return this.scaleResult; + } + } +#endif +} From f4527f602bc53376496618f616977474903fc122 Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Thu, 4 May 2023 15:53:04 -0700 Subject: [PATCH 2/7] update csproj --- .../DurableTask.SqlServer.AzureFunctions.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DurableTask.SqlServer.AzureFunctions/DurableTask.SqlServer.AzureFunctions.csproj b/src/DurableTask.SqlServer.AzureFunctions/DurableTask.SqlServer.AzureFunctions.csproj index 59f8f36..02e9d39 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/DurableTask.SqlServer.AzureFunctions.csproj +++ b/src/DurableTask.SqlServer.AzureFunctions/DurableTask.SqlServer.AzureFunctions.csproj @@ -4,7 +4,7 @@ - netstandard2.1;netcoreapp3.1 + netstandard2.0;netcoreapp3.1 From a469297fd8c6846f4286b7e21bedbc1048f9e0f2 Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Wed, 17 May 2023 11:41:22 -0700 Subject: [PATCH 3/7] added previousWorkerCount as argument for GetMetricsAsync() --- .../SqlMetricsProvider.cs | 6 ++---- src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs | 2 +- src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs index b78b03d..3414bd8 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs @@ -9,20 +9,18 @@ namespace DurableTask.SqlServer.AzureFunctions class SqlMetricsProvider { readonly SqlOrchestrationService service; - readonly int? previousWorkerCount; public SqlMetricsProvider(SqlOrchestrationService service, int? previousWorkerCount = null) { this.service = service; - this.previousWorkerCount = previousWorkerCount; } - public async Task GetMetricsAsync() + public async Task GetMetricsAsync(int? previousWorkerCount = null) { // GetRecommendedReplicaCountAsync will write a trace if the recommendation results // in a worker count that is different from the worker count we pass in as an argument. int recommendedReplicaCount = await this.service.GetRecommendedReplicaCountAsync( - this.previousWorkerCount, + previousWorkerCount, CancellationToken.None); return new SqlScaleMetric { RecommendedReplicaCount = recommendedReplicaCount }; diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs index b60870d..d2adc34 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs @@ -40,7 +40,7 @@ public SqlScaleMonitor(SqlOrchestrationService service, string taskHubName, SqlM /// public async Task GetMetricsAsync() { - return await this.metricsProvider.GetMetricsAsync(); + return await this.metricsProvider.GetMetricsAsync(this.previousWorkerCount); } /// diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs index 444e615..ef8d06b 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs @@ -6,7 +6,7 @@ namespace DurableTask.SqlServer.AzureFunctions using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Host.Scale; -#if NETCOREAPP +#if !NETSTANDARD class SqlTargetScaler : ITargetScaler { readonly SqlMetricsProvider sqlMetricsProvider; From 4b952a541f30df3fb3ac689e14072f0f9362011b Mon Sep 17 00:00:00 2001 From: Varshitha Bachu Date: Wed, 17 May 2023 12:18:14 -0700 Subject: [PATCH 4/7] adding tests --- .../SqlMetricsProvider.cs | 4 +- .../SqlScaleMetric.cs | 2 +- .../SqlTargetScaler.cs | 2 +- .../TargetBasedScalingTests.cs | 59 +++++++++++++++++++ 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 test/DurableTask.SqlServer.AzureFunctions.Tests/TargetBasedScalingTests.cs diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs index 3414bd8..9dd5680 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs @@ -6,7 +6,7 @@ namespace DurableTask.SqlServer.AzureFunctions using System.Threading; using System.Threading.Tasks; - class SqlMetricsProvider + public class SqlMetricsProvider { readonly SqlOrchestrationService service; @@ -15,7 +15,7 @@ public SqlMetricsProvider(SqlOrchestrationService service, int? previousWorkerCo this.service = service; } - public async Task GetMetricsAsync(int? previousWorkerCount = null) + public virtual async Task GetMetricsAsync(int? previousWorkerCount = null) { // GetRecommendedReplicaCountAsync will write a trace if the recommendation results // in a worker count that is different from the worker count we pass in as an argument. diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMetric.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMetric.cs index 1bbd337..2203ccc 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMetric.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMetric.cs @@ -5,7 +5,7 @@ namespace DurableTask.SqlServer.AzureFunctions { using Microsoft.Azure.WebJobs.Host.Scale; - class SqlScaleMetric : ScaleMetrics + public class SqlScaleMetric : ScaleMetrics { public int RecommendedReplicaCount { get; set; } } diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs index ef8d06b..85457dc 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs @@ -7,7 +7,7 @@ namespace DurableTask.SqlServer.AzureFunctions using Microsoft.Azure.WebJobs.Host.Scale; #if !NETSTANDARD - class SqlTargetScaler : ITargetScaler + public class SqlTargetScaler : ITargetScaler { readonly SqlMetricsProvider sqlMetricsProvider; readonly TargetScalerResult scaleResult; diff --git a/test/DurableTask.SqlServer.AzureFunctions.Tests/TargetBasedScalingTests.cs b/test/DurableTask.SqlServer.AzureFunctions.Tests/TargetBasedScalingTests.cs new file mode 100644 index 0000000..d4a7fb5 --- /dev/null +++ b/test/DurableTask.SqlServer.AzureFunctions.Tests/TargetBasedScalingTests.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace DurableTask.SqlServer.AzureFunctions.Tests +{ + using System; + using DurableTask.Core; + using Microsoft.Azure.WebJobs.Extensions.DurableTask; + using Microsoft.Azure.WebJobs.Host.Scale; + using Microsoft.Extensions.Logging; + using Moq; + using Xunit; + using Xunit.Abstractions; + + public class TargetBasedScalingTests + { + readonly Mock metricsProviderMock; + readonly Mock orchestrationServiceMock; + + public TargetBasedScalingTests() + { + this.orchestrationServiceMock = new Mock(MockBehavior.Strict); + + this.metricsProviderMock = new Mock( + MockBehavior.Strict, + null, + null); + } + + [Theory] + [InlineData(0)] + [InlineData(10)] + [InlineData(20)] + public async void TargetBasedScalingTest(int expectedTargetWorkerCount) + { + var durabilityProviderMock = new Mock( + MockBehavior.Strict, + "storageProviderName", + this.orchestrationServiceMock.Object, + new Mock().Object, + "connectionName"); + + SqlScaleMetric sqlScaleMetric = new SqlScaleMetric() + { + RecommendedReplicaCount = expectedTargetWorkerCount, + }; + + this.metricsProviderMock.Setup(m => m.GetMetricsAsync(null)).ReturnsAsync(sqlScaleMetric); + + SqlTargetScaler targetScaler = new SqlTargetScaler( + "functionId", + this.metricsProviderMock.Object); + + TargetScalerResult result = await targetScaler.GetScaleResultAsync(new TargetScalerContext()); + + Assert.Equal(expectedTargetWorkerCount, result.TargetWorkerCount); + } + } +} From c4498c4e73f660b397832a1afbf9a23cae65c31f Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Wed, 9 Oct 2024 18:20:09 -0700 Subject: [PATCH 5/7] PR feedback updates, fix build warnings, update CHANGELOG.md, etc. --- CHANGELOG.md | 8 ++++++- ...urableTask.SqlServer.AzureFunctions.csproj | 8 +++++-- .../SqlDurabilityProvider.cs | 22 ++++++++++++++----- .../SqlDurabilityProviderFactory.cs | 2 +- .../SqlScaleMonitor.cs | 14 ++++++++---- .../SqlTargetScaler.cs | 20 ++++++++++------- src/common.props | 2 +- 7 files changed, 54 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 060bd91..d803e73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,16 @@ # Changelog -## v1.3.1 (Unreleased) +## v1.4.0 + +### New + +* Support for Azure Functions target-based scaling ([#169](https://github.com/microsoft/durabletask-mssql/pull/169)) +* Added `net6.0` TFM to Microsoft.DurableTask.SqlServer.AzureFunctions ### Updates * 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) +* Pin Microsoft.Azure.WebJobs.Extensions.DurableTask dependency to 2.13.7 instead of wildcard to avoid accidental build breaks ## v1.3.0 diff --git a/src/DurableTask.SqlServer.AzureFunctions/DurableTask.SqlServer.AzureFunctions.csproj b/src/DurableTask.SqlServer.AzureFunctions/DurableTask.SqlServer.AzureFunctions.csproj index 805c23c..347943a 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/DurableTask.SqlServer.AzureFunctions.csproj +++ b/src/DurableTask.SqlServer.AzureFunctions/DurableTask.SqlServer.AzureFunctions.csproj @@ -4,7 +4,11 @@ - netstandard2.0;netcoreapp3.1 + netstandard2.0;net6.0 + + + + $(DefineConstants);FUNCTIONS_V4 @@ -16,7 +20,7 @@ - + diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProvider.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProvider.cs index 721c2a9..e1f8de4 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProvider.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProvider.cs @@ -23,6 +23,9 @@ class SqlDurabilityProvider : DurabilityProvider readonly SqlOrchestrationService service; SqlScaleMonitor? scaleMonitor; +#if FUNCTIONS_V4 + SqlTargetScaler? targetScaler; +#endif public SqlDurabilityProvider( SqlOrchestrationService service, @@ -197,12 +200,17 @@ public override bool TryGetScaleMonitor( string storageConnectionString, out IScaleMonitor scaleMonitor) { - SqlMetricsProvider sqlMetricsProvider = new SqlMetricsProvider(this.service); - scaleMonitor = this.scaleMonitor ??= new SqlScaleMonitor(this.service, hubName, sqlMetricsProvider); + if (this.scaleMonitor == null) + { + var sqlMetricsProvider = new SqlMetricsProvider(this.service); + this.scaleMonitor = new SqlScaleMonitor(hubName, sqlMetricsProvider); + } + + scaleMonitor = this.scaleMonitor; return true; } -#if NETCOREAPP +#if FUNCTIONS_V4 public override bool TryGetTargetScaler( string functionId, string functionName, @@ -210,9 +218,13 @@ public override bool TryGetTargetScaler( string connectionName, out ITargetScaler targetScaler) { - SqlMetricsProvider sqlMetricsProvider = new SqlMetricsProvider(this.service); - targetScaler = new SqlTargetScaler(functionId, sqlMetricsProvider); + if (this.targetScaler == null) + { + var sqlMetricsProvider = new SqlMetricsProvider(this.service); + this.targetScaler = new SqlTargetScaler(hubName, sqlMetricsProvider); + } + targetScaler = this.targetScaler; return true; } #endif diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProviderFactory.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProviderFactory.cs index 6ceb1f8..0510f16 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProviderFactory.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlDurabilityProviderFactory.cs @@ -66,7 +66,7 @@ public DurabilityProvider GetDurabilityProvider(DurableClientAttribute attribute lock (this.clientProviders) { string key = GetDurabilityProviderKey(attribute); - if (this.clientProviders.TryGetValue(key, out DurabilityProvider clientProvider)) + if (this.clientProviders.TryGetValue(key, out DurabilityProvider? clientProvider)) { return clientProvider; } diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs index d2adc34..66022e1 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlScaleMonitor.cs @@ -19,15 +19,21 @@ class SqlScaleMonitor : IScaleMonitor static readonly ScaleStatus NoScaleVote = new ScaleStatus { Vote = ScaleVote.None }; static readonly ScaleStatus ScaleOutVote = new ScaleStatus { Vote = ScaleVote.ScaleOut }; - readonly SqlOrchestrationService service; readonly SqlMetricsProvider metricsProvider; int? previousWorkerCount = -1; - public SqlScaleMonitor(SqlOrchestrationService service, string taskHubName, SqlMetricsProvider sqlMetricsProvider) + public SqlScaleMonitor(string taskHubName, SqlMetricsProvider sqlMetricsProvider) { - this.service = service ?? throw new ArgumentNullException(nameof(service)); - this.Descriptor = new ScaleMonitorDescriptor($"DurableTask-SqlServer:{taskHubName ?? "default"}"); + // Scalers in Durable Functions are shared for all functions in the same task hub. + // So instead of using a function ID, we use the task hub name as the basis for the descriptor ID. + string id = $"DurableTask-SqlServer:{taskHubName ?? "default"}"; + +#if FUNCTIONS_V4 + this.Descriptor = new ScaleMonitorDescriptor(id: id, functionId: id); +#else + this.Descriptor = new ScaleMonitorDescriptor(id); +#endif this.metricsProvider = sqlMetricsProvider ?? throw new ArgumentNullException(nameof(sqlMetricsProvider)); } diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs index 85457dc..faed8db 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs @@ -1,22 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#if FUNCTIONS_V4 namespace DurableTask.SqlServer.AzureFunctions { using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Host.Scale; -#if !NETSTANDARD public class SqlTargetScaler : ITargetScaler { readonly SqlMetricsProvider sqlMetricsProvider; - readonly TargetScalerResult scaleResult; - public SqlTargetScaler(string functionId, SqlMetricsProvider sqlMetricsProvider) + public SqlTargetScaler(string taskHubName, SqlMetricsProvider sqlMetricsProvider) { this.sqlMetricsProvider = sqlMetricsProvider; - this.scaleResult = new TargetScalerResult(); - this.TargetScalerDescriptor = new TargetScalerDescriptor(functionId); + + // Scalers in Durable Functions are shared for all functions in the same task hub. + // So instead of using a function ID, we use the task hub name as the basis for the descriptor ID. + string id = $"DurableTask-SqlServer:{taskHubName ?? "default"}"; + this.TargetScalerDescriptor = new TargetScalerDescriptor(id); } public TargetScalerDescriptor TargetScalerDescriptor { get; private set; } @@ -24,9 +26,11 @@ public SqlTargetScaler(string functionId, SqlMetricsProvider sqlMetricsProvider) public async Task GetScaleResultAsync(TargetScalerContext context) { SqlScaleMetric sqlScaleMetric = await this.sqlMetricsProvider.GetMetricsAsync(); - this.scaleResult.TargetWorkerCount = sqlScaleMetric.RecommendedReplicaCount; - return this.scaleResult; + return new TargetScalerResult + { + TargetWorkerCount = sqlScaleMetric.RecommendedReplicaCount, + }; } } -#endif } +#endif diff --git a/src/common.props b/src/common.props index ace5c3e..6ee3a62 100644 --- a/src/common.props +++ b/src/common.props @@ -16,7 +16,7 @@ 1 - 3 + 4 0 $(MajorVersion).$(MinorVersion).$(PatchVersion) From b9d68913479f72c9fff8cd7f8e118929818de8cb Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Wed, 9 Oct 2024 18:25:26 -0700 Subject: [PATCH 6/7] Fix failing tests caused by version update --- src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs | 2 +- .../Integration/DatabaseManagement.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs index faed8db..b98169c 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs @@ -21,7 +21,7 @@ public SqlTargetScaler(string taskHubName, SqlMetricsProvider sqlMetricsProvider this.TargetScalerDescriptor = new TargetScalerDescriptor(id); } - public TargetScalerDescriptor TargetScalerDescriptor { get; private set; } + public TargetScalerDescriptor TargetScalerDescriptor { get; } public async Task GetScaleResultAsync(TargetScalerContext context) { diff --git a/test/DurableTask.SqlServer.Tests/Integration/DatabaseManagement.cs b/test/DurableTask.SqlServer.Tests/Integration/DatabaseManagement.cs index 3f4f024..40e1692 100644 --- a/test/DurableTask.SqlServer.Tests/Integration/DatabaseManagement.cs +++ b/test/DurableTask.SqlServer.Tests/Integration/DatabaseManagement.cs @@ -503,7 +503,7 @@ async Task ValidateDatabaseSchemaAsync(TestDatabase database, string schemaName database.ConnectionString, schemaName); Assert.Equal(1, currentSchemaVersion.Major); - Assert.Equal(3, currentSchemaVersion.Minor); + Assert.Equal(4, currentSchemaVersion.Minor); Assert.Equal(0, currentSchemaVersion.Patch); } From 830934f72ce15d25ff12fd7371631ca7480c58a8 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Thu, 10 Oct 2024 10:26:46 -0700 Subject: [PATCH 7/7] PR feedback and minor cleanup --- .../SqlMetricsProvider.cs | 2 +- .../SqlTargetScaler.cs | 3 ++- .../TargetBasedScalingTests.cs | 13 +++++-------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs index 9dd5680..8c17b88 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlMetricsProvider.cs @@ -10,7 +10,7 @@ public class SqlMetricsProvider { readonly SqlOrchestrationService service; - public SqlMetricsProvider(SqlOrchestrationService service, int? previousWorkerCount = null) + public SqlMetricsProvider(SqlOrchestrationService service) { this.service = service; } diff --git a/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs b/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs index b98169c..116efae 100644 --- a/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs +++ b/src/DurableTask.SqlServer.AzureFunctions/SqlTargetScaler.cs @@ -4,6 +4,7 @@ #if FUNCTIONS_V4 namespace DurableTask.SqlServer.AzureFunctions { + using System; using System.Threading.Tasks; using Microsoft.Azure.WebJobs.Host.Scale; @@ -28,7 +29,7 @@ public async Task GetScaleResultAsync(TargetScalerContext co SqlScaleMetric sqlScaleMetric = await this.sqlMetricsProvider.GetMetricsAsync(); return new TargetScalerResult { - TargetWorkerCount = sqlScaleMetric.RecommendedReplicaCount, + TargetWorkerCount = Math.Max(0, sqlScaleMetric.RecommendedReplicaCount), }; } } diff --git a/test/DurableTask.SqlServer.AzureFunctions.Tests/TargetBasedScalingTests.cs b/test/DurableTask.SqlServer.AzureFunctions.Tests/TargetBasedScalingTests.cs index d4a7fb5..9c6a793 100644 --- a/test/DurableTask.SqlServer.AzureFunctions.Tests/TargetBasedScalingTests.cs +++ b/test/DurableTask.SqlServer.AzureFunctions.Tests/TargetBasedScalingTests.cs @@ -3,14 +3,11 @@ namespace DurableTask.SqlServer.AzureFunctions.Tests { - using System; using DurableTask.Core; using Microsoft.Azure.WebJobs.Extensions.DurableTask; using Microsoft.Azure.WebJobs.Host.Scale; - using Microsoft.Extensions.Logging; using Moq; using Xunit; - using Xunit.Abstractions; public class TargetBasedScalingTests { @@ -21,10 +18,10 @@ public TargetBasedScalingTests() { this.orchestrationServiceMock = new Mock(MockBehavior.Strict); + SqlOrchestrationService? nullServiceArg = null; // not needed for this test this.metricsProviderMock = new Mock( - MockBehavior.Strict, - null, - null); + behavior: MockBehavior.Strict, + nullServiceArg); } [Theory] @@ -40,14 +37,14 @@ public async void TargetBasedScalingTest(int expectedTargetWorkerCount) new Mock().Object, "connectionName"); - SqlScaleMetric sqlScaleMetric = new SqlScaleMetric() + var sqlScaleMetric = new SqlScaleMetric() { RecommendedReplicaCount = expectedTargetWorkerCount, }; this.metricsProviderMock.Setup(m => m.GetMetricsAsync(null)).ReturnsAsync(sqlScaleMetric); - SqlTargetScaler targetScaler = new SqlTargetScaler( + var targetScaler = new SqlTargetScaler( "functionId", this.metricsProviderMock.Object);