Skip to content

Commit c345f71

Browse files
authored
Configure MS SQL ScaledObject Query Based on Durable Task Configuration (#2771)
* Initial fix * Minor change
1 parent ba366eb commit c345f71

File tree

4 files changed

+166
-5
lines changed

4 files changed

+166
-5
lines changed

src/Azure.Functions.Cli/Kubernetes/KEDA/V2/KedaV2Resource.cs

+13-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Globalization;
34
using System.Linq;
45
using Azure.Functions.Cli.Kubernetes.KEDA.Models;
56
using Azure.Functions.Cli.Kubernetes.KEDA.V2.Models;
@@ -47,31 +48,38 @@ public override IKubernetesResource GetKubernetesResource(string name, string @n
4748
};
4849
}
4950

50-
private static bool IsDurable(JToken trigger) =>
51+
private static bool IsDurable(JToken trigger) =>
5152
trigger["type"].ToString().Equals("orchestrationTrigger", StringComparison.OrdinalIgnoreCase) ||
5253
trigger["type"].ToString().Equals("activityTrigger", StringComparison.OrdinalIgnoreCase) ||
5354
trigger["type"].ToString().Equals("entityTrigger", StringComparison.OrdinalIgnoreCase);
5455

5556
private static IEnumerable<ScaledObjectTriggerV1Alpha1> GetDurableScalar(JObject hostJson)
5657
{
5758
// Reference: https://docs.microsoft.com/azure/azure-functions/durable/durable-functions-bindings#durable-functions-2-0-host-json
58-
JObject storageProviderConfig = hostJson.SelectToken("extensions.durableTask.storageProvider") as JObject;
59-
string storageType = storageProviderConfig?["type"]?.ToString();
59+
DurableTaskConfig durableTaskConfig = hostJson.SelectToken("extensions.durableTask").ToObject<DurableTaskConfig>();
60+
string storageType = durableTaskConfig?.StorageProvider?["type"]?.ToString();
6061

6162
// Custom storage types are supported starting in Durable Functions v2.4.2
6263
if (string.Equals(storageType, "MicrosoftSQL", StringComparison.OrdinalIgnoreCase) ||
6364
string.Equals(storageType, "mssql", StringComparison.OrdinalIgnoreCase))
6465
{
66+
// By default, max 10 orchestrations and 1 activity per replica
67+
string query = string.Format(
68+
CultureInfo.InvariantCulture,
69+
"SELECT dt.GetScaleRecommendation({0}, {1})",
70+
durableTaskConfig.MaxConcurrentOrchestratorFunctions,
71+
durableTaskConfig.MaxConcurrentActivityFunctions);
72+
6573
yield return new ScaledObjectTriggerV1Alpha1
6674
{
6775
// MSSQL scaler reference: https://keda.sh/docs/2.2/scalers/mssql/
6876
Type = "mssql",
6977
Metadata = new Dictionary<string, string>
7078
{
7179
// Durable SQL scaling: https://microsoft.github.io/durabletask-mssql/#/scaling?id=worker-auto-scale
72-
["query"] = "SELECT dt.GetScaleRecommendation(10, 1)", // max 10 orchestrations and 1 activity per replica
80+
["query"] = query,
7381
["targetValue"] = "1",
74-
["connectionStringFromEnv"] = storageProviderConfig?["connectionStringName"]?.ToString(),
82+
["connectionStringFromEnv"] = durableTaskConfig.StorageProvider["connectionStringName"]?.ToString(),
7583
}
7684
};
7785
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using Newtonsoft.Json;
2+
using Newtonsoft.Json.Linq;
3+
using System.ComponentModel;
4+
5+
namespace Azure.Functions.Cli.Kubernetes.Models
6+
{
7+
internal class DurableTaskConfig
8+
{
9+
[JsonProperty("maxConcurrentOrchestratorFunctions")]
10+
public int MaxConcurrentOrchestratorFunctions { get; set; } = 10;
11+
12+
[JsonProperty("maxConcurrentActivityFunctions")]
13+
public int MaxConcurrentActivityFunctions { get; set; } = 1;
14+
15+
[JsonProperty("storageProvider")]
16+
public JObject StorageProvider { get; set; }
17+
}
18+
}

test/Azure.Functions.Cli.Tests/Azure.Functions.Cli.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<PackageReference Include="FluentAssertions" Version="5.2.0" />
1616
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.6.2" />
1717
<PackageReference Include="Moq" Version="4.8.2" />
18+
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
1819
<PackageReference Include="NSubstitute" Version="3.1.0" />
1920
<PackageReference Include="Octokit" Version="0.29.0" />
2021
<PackageReference Include="RichardSzalay.MockHttp" Version="5.0.0" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Azure.Functions.Cli.Kubernetes.KEDA.Models;
4+
using Azure.Functions.Cli.Kubernetes.KEDA.V2;
5+
using Azure.Functions.Cli.Kubernetes.KEDA.V2.Models;
6+
using Azure.Functions.Cli.Kubernetes.Models;
7+
using Azure.Functions.Cli.Kubernetes.Models.Kubernetes;
8+
using Newtonsoft.Json.Linq;
9+
using Xunit;
10+
11+
namespace Azure.Functions.Cli.Tests
12+
{
13+
public class KedaV2ResourceTests
14+
{
15+
private readonly Dictionary<string, JObject> _functions = new Dictionary<string, JObject>();
16+
17+
public KedaV2ResourceTests()
18+
{
19+
_functions.Add(
20+
"HelloOrchestration",
21+
JObject.Parse(@"
22+
{
23+
""generatedBy"": ""Microsoft.NET.Sdk.Functions-3.0.13"",
24+
""configurationSource"": ""attributes"",
25+
""bindings"": [
26+
{
27+
""type"": ""orchestrationTrigger"",
28+
""name"": ""context""
29+
}
30+
],
31+
""disabled"": false,
32+
""scriptFile"": ""../bin/Functions.HelloWorld.dll"",
33+
""entryPoint"": ""Functions.HelloWorld.HelloOrchestration""
34+
}
35+
"));
36+
}
37+
38+
[Theory]
39+
[InlineData(null)]
40+
[InlineData("{ }")]
41+
[InlineData("{ \"type\": \"vnext\" }")]
42+
public void GetUnsupportedDurableScalar(string providerJson)
43+
{
44+
string hostSnippet = @"
45+
{
46+
""extensions"": {
47+
""maxConcurrentOrchestratorFunctions"": 5,
48+
""maxConcurrentActivityFunctions"": 10,
49+
""durableTask"": {
50+
}
51+
}
52+
}
53+
";
54+
55+
JObject hostConfig = JObject.Parse(hostSnippet);
56+
JObject durableTaskConfig = hostConfig.SelectToken("extensions.durableTask") as JObject;
57+
58+
if (providerJson != null)
59+
durableTaskConfig.Add("storageProvider", JObject.Parse(providerJson));
60+
61+
KedaV2Resource resource = new KedaV2Resource();
62+
ScaledObjectKedaV2 scaledObject = resource.GetKubernetesResource(
63+
"HelloWorld",
64+
"default",
65+
new TriggersPayload { HostJson = hostConfig, FunctionsJson = _functions },
66+
new DeploymentV1Apps { Metadata = new ObjectMetadataV1 { Name = "HelloDeployment" } },
67+
30,
68+
300,
69+
1,
70+
8) as ScaledObjectKedaV2;
71+
72+
Assert.NotNull(scaledObject);
73+
Assert.Empty(scaledObject.Spec.Triggers);
74+
}
75+
76+
[Theory]
77+
[InlineData(null, null, 10, 1)]
78+
[InlineData(3, 6, 3, 6)]
79+
public void GetMsSqlDurableScalar(
80+
int? configuredMaxOrchestrations,
81+
int? configuredMaxActivities,
82+
int expectedMaxOrchestrations,
83+
int expectedMaxActivities)
84+
{
85+
string hostSnippet = @"
86+
{
87+
""extensions"": {
88+
""durableTask"": {
89+
""storageProvider"": {
90+
""type"": ""mssql"",
91+
""connectionStringName"": ""MySqlConnection"",
92+
""taskEventLockTimeout"": ""00:01:00"",
93+
""partitionCount"": 5
94+
}
95+
}
96+
}
97+
}
98+
";
99+
100+
JObject hostConfig = JObject.Parse(hostSnippet);
101+
JObject durableTaskConfig = hostConfig.SelectToken("extensions.durableTask") as JObject;
102+
103+
if (configuredMaxOrchestrations.HasValue)
104+
durableTaskConfig.Add("maxConcurrentOrchestratorFunctions", configuredMaxOrchestrations);
105+
106+
if (configuredMaxActivities.HasValue)
107+
durableTaskConfig.Add("maxConcurrentActivityFunctions", configuredMaxActivities);
108+
109+
KedaV2Resource resource = new KedaV2Resource();
110+
ScaledObjectKedaV2 scaledObject = resource.GetKubernetesResource(
111+
"HelloWorld",
112+
"default",
113+
new TriggersPayload { HostJson = hostConfig, FunctionsJson = _functions },
114+
new DeploymentV1Apps { Metadata = new ObjectMetadataV1 { Name = "HelloDeployment" } },
115+
30,
116+
300,
117+
1,
118+
8) as ScaledObjectKedaV2;
119+
120+
Assert.NotNull(scaledObject);
121+
AssertMsSqlDurableScalar(scaledObject.Spec.Triggers.Single(), expectedMaxOrchestrations, expectedMaxActivities, "MySqlConnection");
122+
}
123+
124+
private static void AssertMsSqlDurableScalar(ScaledObjectTriggerV1Alpha1 actual, int maxOrchestrations, int maxActivities, string connectionString)
125+
{
126+
Assert.NotNull(actual);
127+
Assert.Equal("mssql", actual.Type);
128+
Assert.Equal(3, actual.Metadata.Count);
129+
Assert.Equal($"SELECT dt.GetScaleRecommendation({maxOrchestrations}, {maxActivities})", actual.Metadata["query"]);
130+
Assert.Equal("1", actual.Metadata["targetValue"]);
131+
Assert.Equal(connectionString, actual.Metadata["connectionStringFromEnv"]);
132+
}
133+
}
134+
}

0 commit comments

Comments
 (0)