Skip to content

Commit 8359496

Browse files
committed
Make scalers into per-task hub singletons
1 parent ac1a650 commit 8359496

File tree

4 files changed

+68
-37
lines changed

4 files changed

+68
-37
lines changed

src/WebJobs.Extensions.DurableTask/AzureStorageDurabilityProvider.cs

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ internal class AzureStorageDurabilityProvider : DurabilityProvider
3535
private readonly JObject storageOptionsJson;
3636
private readonly ILogger logger;
3737

38+
private readonly object initLock = new object();
39+
40+
#if !FUNCTIONS_V1
41+
private DurableTaskScaleMonitor singletonScaleMonitor;
42+
#endif
43+
44+
#if FUNCTIONS_V3_OR_GREATER
45+
private DurableTaskTargetScaler singletonTargetScaler;
46+
#endif
47+
3848
public AzureStorageDurabilityProvider(
3949
AzureStorageOrchestrationService service,
4050
IStorageAccountProvider storageAccountProvider,
@@ -226,12 +236,11 @@ internal static OrchestrationInstanceStatusQueryCondition ConvertWebjobsDurableC
226236
#if !FUNCTIONS_V1
227237

228238
internal DurableTaskMetricsProvider GetMetricsProvider(
229-
string functionName,
230239
string hubName,
231240
CloudStorageAccount storageAccount,
232241
ILogger logger)
233242
{
234-
return new DurableTaskMetricsProvider(functionName, hubName, logger, performanceMonitor: null, storageAccount);
243+
return new DurableTaskMetricsProvider(hubName, logger, performanceMonitor: null, storageAccount);
235244
}
236245

237246
/// <inheritdoc/>
@@ -242,16 +251,22 @@ public override bool TryGetScaleMonitor(
242251
string connectionName,
243252
out IScaleMonitor scaleMonitor)
244253
{
245-
CloudStorageAccount storageAccount = this.storageAccountProvider.GetStorageAccountDetails(connectionName).ToCloudStorageAccount();
246-
DurableTaskMetricsProvider metricsProvider = this.GetMetricsProvider(functionName, hubName, storageAccount, this.logger);
247-
scaleMonitor = new DurableTaskScaleMonitor(
248-
functionId,
249-
functionName,
250-
hubName,
251-
storageAccount,
252-
this.logger,
253-
metricsProvider);
254-
return true;
254+
lock (this.initLock)
255+
{
256+
if (this.singletonScaleMonitor == null)
257+
{
258+
CloudStorageAccount storageAccount = this.storageAccountProvider.GetStorageAccountDetails(connectionName).ToCloudStorageAccount();
259+
DurableTaskMetricsProvider metricsProvider = this.GetMetricsProvider(hubName, storageAccount, this.logger);
260+
this.singletonScaleMonitor = new DurableTaskScaleMonitor(
261+
hubName,
262+
storageAccount,
263+
this.logger,
264+
metricsProvider);
265+
}
266+
267+
scaleMonitor = this.singletonScaleMonitor;
268+
return true;
269+
}
255270
}
256271

257272
#endif
@@ -263,11 +278,23 @@ public override bool TryGetTargetScaler(
263278
string connectionName,
264279
out ITargetScaler targetScaler)
265280
{
266-
// This is only called by the ScaleController, it doesn't run in the Functions Host process.
267-
CloudStorageAccount storageAccount = this.storageAccountProvider.GetStorageAccountDetails(connectionName).ToCloudStorageAccount();
268-
DurableTaskMetricsProvider metricsProvider = this.GetMetricsProvider(functionName, hubName, storageAccount, this.logger);
269-
targetScaler = new DurableTaskTargetScaler(functionId, metricsProvider, this, this.logger);
270-
return true;
281+
lock (this.initLock)
282+
{
283+
if (this.singletonTargetScaler == null)
284+
{
285+
// This is only called by the ScaleController, it doesn't run in the Functions Host process.
286+
CloudStorageAccount storageAccount = this.storageAccountProvider.GetStorageAccountDetails(connectionName).ToCloudStorageAccount();
287+
DurableTaskMetricsProvider metricsProvider = this.GetMetricsProvider(hubName, storageAccount, this.logger);
288+
289+
// Scalers in Durable Functions are shared for all functions in the same task hub.
290+
// So instead of using a function ID, we use the task hub name as the basis for the descriptor ID.
291+
string id = $"DurableTask-AzureStorage:{hubName ?? "default"}";
292+
this.singletonTargetScaler = new DurableTaskTargetScaler(id, metricsProvider, this, this.logger);
293+
}
294+
295+
targetScaler = this.singletonTargetScaler;
296+
return true;
297+
}
271298
}
272299
#endif
273300
}

src/WebJobs.Extensions.DurableTask/Listener/DurableTaskMetricsProvider.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,18 @@ namespace Microsoft.Azure.WebJobs.Extensions.DurableTask
1313
{
1414
internal class DurableTaskMetricsProvider
1515
{
16-
private readonly string functionName;
1716
private readonly string hubName;
1817
private readonly ILogger logger;
1918
private readonly CloudStorageAccount storageAccount;
2019

2120
private DisconnectedPerformanceMonitor performanceMonitor;
2221

23-
public DurableTaskMetricsProvider(string functionName, string hubName, ILogger logger, DisconnectedPerformanceMonitor performanceMonitor, CloudStorageAccount storageAccount)
22+
public DurableTaskMetricsProvider(
23+
string hubName,
24+
ILogger logger,
25+
DisconnectedPerformanceMonitor performanceMonitor,
26+
CloudStorageAccount storageAccount)
2427
{
25-
this.functionName = functionName;
2628
this.hubName = hubName;
2729
this.logger = logger;
2830
this.performanceMonitor = performanceMonitor;
@@ -42,7 +44,7 @@ public virtual async Task<DurableTaskTriggerMetrics> GetMetricsAsync()
4244
}
4345
catch (StorageException e)
4446
{
45-
this.logger.LogWarning("{details}. Function: {functionName}. HubName: {hubName}.", e.ToString(), this.functionName, this.hubName);
47+
this.logger.LogWarning("{details}. HubName: {hubName}.", e.ToString(), this.hubName);
4648
}
4749

4850
if (heartbeat != null)

src/WebJobs.Extensions.DurableTask/Listener/DurableTaskScaleMonitor.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ namespace Microsoft.Azure.WebJobs.Extensions.DurableTask
1616
{
1717
internal sealed class DurableTaskScaleMonitor : IScaleMonitor<DurableTaskTriggerMetrics>
1818
{
19-
private readonly string functionId;
20-
private readonly string functionName;
2119
private readonly string hubName;
2220
private readonly CloudStorageAccount storageAccount;
2321
private readonly ScaleMonitorDescriptor scaleMonitorDescriptor;
@@ -27,30 +25,29 @@ internal sealed class DurableTaskScaleMonitor : IScaleMonitor<DurableTaskTrigger
2725
private DisconnectedPerformanceMonitor performanceMonitor;
2826

2927
public DurableTaskScaleMonitor(
30-
string functionId,
31-
string functionName,
3228
string hubName,
3329
CloudStorageAccount storageAccount,
3430
ILogger logger,
3531
DurableTaskMetricsProvider durableTaskMetricsProvider,
3632
DisconnectedPerformanceMonitor performanceMonitor = null)
3733
{
38-
this.functionId = functionId;
39-
this.functionName = functionName;
4034
this.hubName = hubName;
4135
this.storageAccount = storageAccount;
4236
this.logger = logger;
4337
this.performanceMonitor = performanceMonitor;
4438
this.durableTaskMetricsProvider = durableTaskMetricsProvider;
4539

4640
#if FUNCTIONS_V3_OR_GREATER
47-
this.scaleMonitorDescriptor = new ScaleMonitorDescriptor($"{this.functionId}-DurableTaskTrigger-{this.hubName}".ToLower(), this.functionId);
41+
// Scalers in Durable Functions are shared for all functions in the same task hub.
42+
// So instead of using a function ID, we use the task hub name as the basis for the descriptor ID.
43+
string id = $"DurableTask-AzureStorage:{hubName ?? "default"}";
44+
this.scaleMonitorDescriptor = new ScaleMonitorDescriptor(id: id, functionId: id);
4845
#else
4946
#pragma warning disable CS0618 // Type or member is obsolete.
5047

5148
// We need this because the new ScaleMonitorDescriptor constructor is not compatible with the WebJobs version of Functions V1 and V2.
5249
// Technically, it is also not available in Functions V3, but we don't have a TFM allowing us to differentiate between Functions V3 and V4.
53-
this.scaleMonitorDescriptor = new ScaleMonitorDescriptor($"{this.functionId}-DurableTaskTrigger-{this.hubName}".ToLower());
50+
this.scaleMonitorDescriptor = new ScaleMonitorDescriptor($"DurableTaskTrigger-{this.hubName}".ToLower());
5451
#pragma warning restore CS0618 // Type or member is obsolete. However, the new interface is not compatible with Functions V2 and V1
5552
#endif
5653
}
@@ -150,9 +147,10 @@ private ScaleStatus GetScaleStatusCore(int workerCount, DurableTaskTriggerMetric
150147
if (writeToUserLogs)
151148
{
152149
this.logger.LogInformation(
153-
$"Durable Functions Trigger Scale Decision: {scaleStatus.Vote.ToString()}, Reason: {scaleRecommendation?.Reason}",
150+
"Durable Functions Trigger Scale Decision for {TaskHub}: {Vote}, Reason: {Reason}",
154151
this.hubName,
155-
this.functionName);
152+
scaleStatus.Vote,
153+
scaleRecommendation?.Reason);
156154
}
157155

158156
return scaleStatus;

src/WebJobs.Extensions.DurableTask/Listener/DurableTaskTargetScaler.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,18 @@ internal class DurableTaskTargetScaler : ITargetScaler
1919
private readonly TargetScalerResult scaleResult;
2020
private readonly DurabilityProvider durabilityProvider;
2121
private readonly ILogger logger;
22-
private readonly string functionId;
22+
private readonly string scaler;
2323

24-
public DurableTaskTargetScaler(string functionId, DurableTaskMetricsProvider metricsProvider, DurabilityProvider durabilityProvider, ILogger logger)
24+
public DurableTaskTargetScaler(
25+
string scalerId,
26+
DurableTaskMetricsProvider metricsProvider,
27+
DurabilityProvider durabilityProvider,
28+
ILogger logger)
2529
{
26-
this.functionId = functionId;
30+
this.scaler = scalerId;
2731
this.metricsProvider = metricsProvider;
2832
this.scaleResult = new TargetScalerResult();
29-
this.TargetScalerDescriptor = new TargetScalerDescriptor(this.functionId);
33+
this.TargetScalerDescriptor = new TargetScalerDescriptor(this.scaler);
3034
this.durabilityProvider = durabilityProvider;
3135
this.logger = logger;
3236
}
@@ -68,7 +72,7 @@ public async Task<TargetScalerResult> GetScaleResultAsync(TargetScalerContext co
6872
// and the ScaleController is injecting it's own custom ILogger implementation that forwards logs to Kusto.
6973
var metricsLog = $"Metrics: workItemQueueLength={workItemQueueLength}. controlQueueLengths={serializedControlQueueLengths}. " +
7074
$"maxConcurrentOrchestrators={this.MaxConcurrentOrchestrators}. maxConcurrentActivities={this.MaxConcurrentActivities}";
71-
var scaleControllerLog = $"Target worker count for '{this.functionId}' is '{numWorkersToRequest}'. " +
75+
var scaleControllerLog = $"Target worker count for '{this.scaler}' is '{numWorkersToRequest}'. " +
7276
metricsLog;
7377

7478
// target worker count should never be negative
@@ -85,7 +89,7 @@ public async Task<TargetScalerResult> GetScaleResultAsync(TargetScalerContext co
8589
// We want to augment the exception with metrics information for investigation purposes
8690
var metricsLog = $"Metrics: workItemQueueLength={metrics?.WorkItemQueueLength}. controlQueueLengths={metrics?.ControlQueueLengths}. " +
8791
$"maxConcurrentOrchestrators={this.MaxConcurrentOrchestrators}. maxConcurrentActivities={this.MaxConcurrentActivities}";
88-
var errorLog = $"Error: target worker count for '{this.functionId}' resulted in exception. " + metricsLog;
92+
var errorLog = $"Error: target worker count for '{this.scaler}' resulted in exception. " + metricsLog;
8993
throw new Exception(errorLog, ex);
9094
}
9195
}

0 commit comments

Comments
 (0)