Skip to content

Commit d1067c5

Browse files
authored
Updating end of interval Flex metrics publishing (#10210)
1 parent ac455ef commit d1067c5

File tree

2 files changed

+144
-28
lines changed

2 files changed

+144
-28
lines changed

src/WebJobs.Script.WebHost/Metrics/FlexConsumptionMetricsPublisher.cs

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,19 @@ internal void Initialize()
9191
IsAlwaysReady = _environment.GetEnvironmentVariable(EnvironmentSettingNames.FunctionsAlwaysReadyInstance) == "1";
9292
}
9393

94-
internal async Task OnPublishMetrics()
94+
internal async Task OnPublishMetrics(DateTime now)
9595
{
9696
try
9797
{
98+
lock (_lock)
99+
{
100+
if (ActiveFunctionCount > 0)
101+
{
102+
// at the end of an interval, we'll meter any outstanding activity up to the end of the interval
103+
MeterCurrentActiveInterval(now);
104+
}
105+
}
106+
98107
if (FunctionExecutionCount == 0 && FunctionExecutionTimeMS == 0 && !IsAlwaysReady && !_metricsProvider.HasMetrics())
99108
{
100109
// no activity to report
@@ -137,7 +146,7 @@ internal async Task OnPublishMetrics()
137146

138147
private async void OnFunctionMetricsPublishTimer(object state)
139148
{
140-
await OnPublishMetrics();
149+
await OnPublishMetrics(DateTime.UtcNow);
141150
}
142151

143152
private async Task PublishMetricsAsync(Metrics metrics)
@@ -280,26 +289,7 @@ internal void OnFunctionCompleted(string functionName, string invocationId, Date
280289
{
281290
// We're transitioning from active to inactive, so we need to accumulate the elapsed time
282291
// for this interval.
283-
DateTime adjustedActivityIntervalStart = _currentActivityIntervalStart;
284-
if (_activityIntervalHighWatermark > _currentActivityIntervalStart)
285-
{
286-
// If we've already metered a previous interval past the current time,
287-
// we move forward (since we never want to meter the same interval twice).
288-
adjustedActivityIntervalStart = _activityIntervalHighWatermark;
289-
}
290-
291-
// If the elapsed duration is negative, it means invocations are still before
292-
// the high watermark, so have already been metered.
293-
double elapsedMS = (now - adjustedActivityIntervalStart).TotalMilliseconds;
294-
if (elapsedMS > 0)
295-
{
296-
// Accumulate the duration for this interval, applying the minimum
297-
var duration = Math.Max(elapsedMS, _options.MinimumActivityIntervalMS);
298-
FunctionExecutionTimeMS += RoundUp(duration, _options.MetricsGranularityMS);
299-
300-
// Move the high watermark timestamp forward
301-
_activityIntervalHighWatermark = adjustedActivityIntervalStart.AddMilliseconds(duration);
302-
}
292+
MeterCurrentActiveInterval(now);
303293
}
304294

305295
// for every completed invocation, increment our invocation count
@@ -312,10 +302,36 @@ public void AddFunctionExecutionActivity(string functionName, string invocationI
312302
// nothing to do here - we only care about Started/Completed events.
313303
}
314304

305+
private void MeterCurrentActiveInterval(DateTime now)
306+
{
307+
DateTime adjustedActivityIntervalStart = _currentActivityIntervalStart;
308+
if (_activityIntervalHighWatermark > _currentActivityIntervalStart)
309+
{
310+
// If we've already metered a previous interval past the current time,
311+
// we move forward (since we never want to meter the same interval twice).
312+
adjustedActivityIntervalStart = _activityIntervalHighWatermark;
313+
}
314+
315+
// If the elapsed duration is negative, it means invocations are still before
316+
// the high watermark, so have already been metered.
317+
double elapsedMS = (now - adjustedActivityIntervalStart).TotalMilliseconds;
318+
if (elapsedMS > 0)
319+
{
320+
// Accumulate the duration for this interval, applying minimums and rounding
321+
var duration = Math.Max(elapsedMS, _options.MinimumActivityIntervalMS);
322+
duration = RoundUp(duration, _options.MetricsGranularityMS);
323+
FunctionExecutionTimeMS += (long)duration;
324+
325+
// Move the high watermark timestamp forward to the point
326+
// up to which we've metered
327+
_activityIntervalHighWatermark = adjustedActivityIntervalStart.AddMilliseconds(duration);
328+
}
329+
}
330+
315331
// Rounds up the given metric to a specified granularity. For example, RoundUp(1320.00, 100) = 1400, but RoundUp(1300.00, 100) = 1300.
316-
private static long RoundUp(double metric, int granularity)
332+
private static double RoundUp(double metric, int granularity)
317333
{
318-
return (long)Math.Ceiling(metric / granularity) * granularity;
334+
return Math.Ceiling(metric / granularity) * granularity;
319335
}
320336

321337
public void Dispose()

test/WebJobs.Script.Tests/Metrics/FlexConsumptionMetricsPublisherTests.cs

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public async Task OnPublishMetrics_WritesFileAndResetsCounts(bool isAlwaysReadyI
105105
int delay = 100;
106106
await Task.Delay(delay);
107107

108-
await publisher.OnPublishMetrics();
108+
await publisher.OnPublishMetrics(DateTime.UtcNow);
109109

110110
FileInfo[] files = GetMetricsFilesSafe(_metricsFilePath);
111111

@@ -137,7 +137,7 @@ public async Task OnPublishMetrics_WritesFileAndResetsCounts(bool isAlwaysReadyI
137137
delay = (int)executionDurationMS + 100;
138138
await Task.Delay(delay);
139139

140-
await publisher.OnPublishMetrics();
140+
await publisher.OnPublishMetrics(DateTime.UtcNow);
141141

142142
files = GetMetricsFilesSafe(_metricsFilePath);
143143
Assert.Equal(1, files.Length);
@@ -180,7 +180,7 @@ public async Task OnPublishMetrics_PurgesOldFiles()
180180
int delay = (int)executionDurationMS + 100;
181181
await Task.Delay(delay);
182182

183-
await publisher.OnPublishMetrics();
183+
await publisher.OnPublishMetrics(DateTime.UtcNow);
184184

185185
FileInfo[] files = GetMetricsFilesSafe(_metricsFilePath);
186186
Assert.Equal(5, files.Length);
@@ -437,6 +437,100 @@ public void FunctionsStartStop_MinimumActivityIntervals_Scenario3()
437437
Assert.Equal(4800, publisher.FunctionExecutionTimeMS);
438438
}
439439

440+
[Fact]
441+
public async Task OnPublishMetrics_OutstandingActivityIsPublished()
442+
{
443+
CleanupMetricsFiles();
444+
445+
var publisher = CreatePublisher(metricsPublishInterval: TimeSpan.FromHours(1), inStandbyMode: false);
446+
447+
Assert.Equal(1000, _options.MinimumActivityIntervalMS);
448+
449+
Assert.Equal(0, publisher.ActiveFunctionCount);
450+
Assert.Equal(0, publisher.FunctionExecutionCount);
451+
Assert.Equal(0, publisher.FunctionExecutionTimeMS);
452+
453+
DateTime now = DateTime.UtcNow;
454+
455+
// *** Interval 0 ***
456+
// one function starts but doesn't complete before the interval ends
457+
now += TimeSpan.FromMilliseconds(200);
458+
publisher.OnFunctionStarted("foo", "0", now);
459+
460+
now += TimeSpan.FromMilliseconds(1800);
461+
await publisher.OnPublishMetrics(now);
462+
463+
FileInfo[] files = GetMetricsFilesSafe(_metricsFilePath);
464+
var metrics = await ReadMetricsAsync(files[0].FullName, deleteFile: true);
465+
466+
// we expect to emit metrics for the duration of the active window
467+
// while the function continues running
468+
Assert.Equal(1, publisher.ActiveFunctionCount);
469+
Assert.Equal(0, metrics.ExecutionCount);
470+
Assert.Equal(1800, metrics.ExecutionTimeMS);
471+
472+
// *** Interval 1 ****
473+
// another function starts and a short time later the first completes
474+
now += TimeSpan.FromMilliseconds(100);
475+
publisher.OnFunctionStarted("foo", "1", now);
476+
now += TimeSpan.FromMilliseconds(100);
477+
publisher.OnFunctionCompleted("foo", "0", now);
478+
479+
// a short time later another function starts
480+
// then completes a short time later
481+
now += TimeSpan.FromMilliseconds(700);
482+
publisher.OnFunctionStarted("bar", "2", now);
483+
now += TimeSpan.FromMilliseconds(800);
484+
publisher.OnFunctionCompleted("bar", "2", now);
485+
486+
Assert.Equal(1, publisher.ActiveFunctionCount);
487+
Assert.Equal(2, publisher.FunctionExecutionCount);
488+
Assert.Equal(0, publisher.FunctionExecutionTimeMS);
489+
490+
// wait another 300ms then simulate the end of the interval
491+
now += TimeSpan.FromMilliseconds(300);
492+
await publisher.OnPublishMetrics(now);
493+
494+
files = GetMetricsFilesSafe(_metricsFilePath);
495+
metrics = await ReadMetricsAsync(files[0].FullName, deleteFile: true);
496+
497+
// we expect metrics reflecting the activity during the entire window
498+
Assert.Equal(1, publisher.ActiveFunctionCount);
499+
Assert.Equal(2, metrics.ExecutionCount);
500+
Assert.Equal(2000, metrics.ExecutionTimeMS);
501+
502+
// *** Interval 2 ***
503+
// now, a little while later the invocation completes
504+
// this gets metered for 1000ms (the minimum)
505+
now += TimeSpan.FromMilliseconds(400);
506+
publisher.OnFunctionCompleted("foo", "1", now);
507+
508+
// after a short time another function starts then completes
509+
// because we've metered the previous function for 1000ms,
510+
// this invocation isn't metered (it falls within the previous period)
511+
now += TimeSpan.FromMilliseconds(200);
512+
publisher.OnFunctionStarted("foo", "3", now);
513+
now += TimeSpan.FromMilliseconds(300);
514+
publisher.OnFunctionCompleted("foo", "3", now);
515+
516+
// another function starts and continues running past the
517+
// end of the second interval
518+
// at the end of thie interval, while the function has only
519+
// ran for 700ms this gets rounded up to 1000ms
520+
now += TimeSpan.FromMilliseconds(400);
521+
publisher.OnFunctionStarted("foo", "4", now);
522+
now += TimeSpan.FromMilliseconds(700);
523+
524+
await publisher.OnPublishMetrics(now);
525+
526+
files = GetMetricsFilesSafe(_metricsFilePath);
527+
metrics = await ReadMetricsAsync(files[0].FullName, deleteFile: true);
528+
529+
Assert.Equal(1, publisher.ActiveFunctionCount);
530+
Assert.Equal(2, metrics.ExecutionCount);
531+
Assert.Equal(2000, metrics.ExecutionTimeMS);
532+
}
533+
440534
[Fact]
441535
public void OnFunctionCompleted_NoOutstandingInvocations_IgnoresEvent()
442536
{
@@ -473,9 +567,15 @@ public void CleanupMetricsFiles()
473567
}
474568
}
475569

476-
private static async Task<FlexConsumptionMetricsPublisher.Metrics> ReadMetricsAsync(string metricsFilePath)
570+
private static async Task<FlexConsumptionMetricsPublisher.Metrics> ReadMetricsAsync(string metricsFilePath, bool deleteFile = false)
477571
{
478572
string content = await File.ReadAllTextAsync(metricsFilePath);
573+
574+
if (deleteFile)
575+
{
576+
File.Delete(metricsFilePath);
577+
}
578+
479579
return JsonConvert.DeserializeObject<FlexConsumptionMetricsPublisher.Metrics>(content);
480580
}
481581

0 commit comments

Comments
 (0)