Skip to content

Commit 81d7ff1

Browse files
authored
Fix concurrency issue with metrics gauge value collection (#106175)
* Fix concurrency issue with metrics gauge value collection * Address the feedback
1 parent afa79b3 commit 81d7ff1

File tree

4 files changed

+91
-37
lines changed

4 files changed

+91
-37
lines changed

src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/AggregationManager.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ private void RemoveInstrumentState(Instrument instrument)
328328
}
329329
};
330330
}
331-
else if (genericDefType == typeof(ObservableGauge<>) || genericDefType == typeof(Gauge<>))
331+
else if (genericDefType == typeof(ObservableGauge<>))
332332
{
333333
return () =>
334334
{
@@ -338,6 +338,16 @@ private void RemoveInstrumentState(Instrument instrument)
338338
}
339339
};
340340
}
341+
else if (genericDefType == typeof(Gauge<>))
342+
{
343+
return () =>
344+
{
345+
lock (this)
346+
{
347+
return CheckTimeSeriesAllowed() ? new SynchronousLastValue() : null;
348+
}
349+
};
350+
}
341351
else if (genericDefType == typeof(Histogram<>))
342352
{
343353
return () =>

src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/LastValueAggregator.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Threading;
5+
46
namespace System.Diagnostics.Metrics
57
{
8+
// Aggregator that keeps the last value it received until Collect().
9+
// This class is used with the observable gauge that always called from a single thread during the collection.
10+
// It is safe to use it without synchronization.
611
internal sealed class LastValue : Aggregator
712
{
813
private double? _lastValue;
@@ -32,4 +37,26 @@ internal LastValueStatistics(double? lastValue)
3237

3338
public double? LastValue { get; }
3439
}
40+
41+
// Aggregator that keeps the last value it received.
42+
// This class is used with the synchronous gauge that can be called from multiple threads during the collection.
43+
// It uses volatile read/write to ensure the visibility of the last value.
44+
internal sealed class SynchronousLastValue : Aggregator
45+
{
46+
private double _lastValue;
47+
48+
public override void Update(double value) => Volatile.Write(ref _lastValue, value);
49+
50+
public override IAggregationStatistics Collect() => new SynchronousLastValueStatistics(Volatile.Read(ref _lastValue));
51+
}
52+
53+
internal sealed class SynchronousLastValueStatistics : IAggregationStatistics
54+
{
55+
internal SynchronousLastValueStatistics(double lastValue)
56+
{
57+
LastValue = lastValue;
58+
}
59+
60+
public double LastValue { get; }
61+
}
3562
}

src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MetricsEventSource.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,12 @@ private void ParseSpecs(string? metricsSpecs)
649649
return;
650650
}
651651

652+
if (metricsSpecs.Length == 0)
653+
{
654+
_aggregationManager!.IncludeAll();
655+
return;
656+
}
657+
652658
string[] specStrings = metricsSpecs.Split(s_instrumentSeparators, StringSplitOptions.RemoveEmptyEntries);
653659
foreach (string specString in specStrings)
654660
{
@@ -699,6 +705,11 @@ private static void TransmitMetricValue(Instrument instrument, LabeledAggregatio
699705
Log.GaugeValuePublished(sessionId, instrument.Meter.Name, instrument.Meter.Version, instrument.Name, instrument.Unit, Helpers.FormatTags(stats.Labels),
700706
lastValueStats.LastValue.HasValue ? lastValueStats.LastValue.Value.ToString(CultureInfo.InvariantCulture) : "", instrumentId);
701707
}
708+
else if (stats.AggregationStatistics is SynchronousLastValueStatistics synchronousLastValueStats)
709+
{
710+
Log.GaugeValuePublished(sessionId, instrument.Meter.Name, instrument.Meter.Version, instrument.Name, instrument.Unit, Helpers.FormatTags(stats.Labels),
711+
synchronousLastValueStats.LastValue.ToString(CultureInfo.InvariantCulture), instrumentId);
712+
}
702713
else if (stats.AggregationStatistics is HistogramStatistics histogramStats)
703714
{
704715
Log.HistogramValuePublished(sessionId, instrument.Meter.Name, instrument.Meter.Version, instrument.Name, instrument.Unit, Helpers.FormatTags(stats.Labels), FormatQuantiles(histogramStats.Quantiles),

src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricEventSourceTests.cs

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public class MetricEventSourceTests
2222
const double IntervalSecs = 10;
2323
static readonly TimeSpan s_waitForEventTimeout = TimeSpan.FromSeconds(60);
2424

25+
private const string RuntimeMeterName = "System.Runtime";
26+
2527
public MetricEventSourceTests(ITestOutputHelper output)
2628
{
2729
_output = output;
@@ -35,7 +37,7 @@ public void GetInstanceMethodIsReflectable()
3537
// Even though the the type isn't public this test ensures the GetInstance() API isn't removed or renamed.
3638
Type? metricsEventSourceType = Type.GetType("System.Diagnostics.Metrics.MetricsEventSource, System.Diagnostics.DiagnosticSource", throwOnError: false);
3739
Assert.True(metricsEventSourceType != null, "Unable to get MetricsEventSource type via reflection");
38-
40+
3941
MethodInfo? getInstanceMethod = metricsEventSourceType.GetMethod("GetInstance", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static, null, Type.EmptyTypes, null);
4042
Assert.True(getInstanceMethod != null, "Unable to get MetricsEventSource.GetInstance method via reflection");
4143

@@ -247,7 +249,7 @@ public void SingleListener_Wildcard()
247249
Counter<int> c3 = meter3.CreateCounter<int>("counter3");
248250

249251
EventWrittenEventArgs[] events;
250-
using (MetricsEventListener listener = new MetricsEventListener(_output, MetricsEventListener.TimeSeriesValues, isShared: true, IntervalSecs, "*"))
252+
using (MetricsEventListener listener = new MetricsEventListener(_output, MetricsEventListener.TimeSeriesValues, isShared: true, IntervalSecs, ""))
251253
{
252254
listener.WaitForCollectionStop(s_waitForEventTimeout, 1);
253255
c.Add(5);
@@ -1227,7 +1229,7 @@ public void EventSourcePublishesMissingDataPoints()
12271229
AssertBeginInstrumentReportingEventsPresent(events, c, oc, og, h, udc, oudc, g);
12281230
AssertInitialEnumerationCompleteEventPresent(events);
12291231
AssertCounterEventsPresent(events, meter.Name, c.Name, "", "", ("5", "5"), ("0", "5"), ("12", "17"));
1230-
AssertGaugeEventsPresent(events, meter.Name, g.Name, "", "", "-123", "", "123", "");
1232+
AssertGaugeEventsPresent(events, meter.Name, g.Name, "", "", "-123", "-123", "123", "123");
12311233
AssertCounterEventsPresent(events, meter.Name, oc.Name, "", "", ("", "17"), ("0", "17"), ("14", "31"), ("0", "31"));
12321234
AssertGaugeEventsPresent(events, meter.Name, og.Name, "", "", "18", "", "36", "");
12331235
AssertHistogramEventsPresent(events, meter.Name, h.Name, "", "", ("0.5=19;0.95=19;0.99=19", "1", "19"), ("", "0", "0"), ("0.5=26;0.95=26;0.99=26", "1", "26"), ("", "0", "0"));
@@ -1290,36 +1292,40 @@ public void EventSourcePublishesEndEventsOnMeterDispose()
12901292
AssertEndInstrumentReportingEventsPresent(events, c, oc, og, udc, oudc, g);
12911293
}
12921294

1293-
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
1295+
[ConditionalFact(typeof(MetricEventSourceTests), nameof(IsNotBrowserAndRemoteExecuteSupported))]
12941296
[OuterLoop("Slow and has lots of console spew")]
12951297
public void EventSourcePublishesInstruments()
12961298
{
1297-
object scope = new object();
1299+
RemoteExecutor.Invoke(static () =>
1300+
{
12981301

1299-
using Meter meterA = new Meter("TestMeter10", null, null, scope);
1300-
using Meter meterB = new Meter("TestMeter11", null, new TagList() { { "Mk1", "Mv1" }, { "Mk2", null } }, scope);
1301-
Counter<int> c = meterA.CreateCounter<int>("counter1", "hat", "Fooz!!");
1302-
Gauge<int> g = meterA.CreateGauge<int>("gauge1", "C", "Temperature");
1303-
int counterState = 3;
1304-
ObservableCounter<int> oc = meterA.CreateObservableCounter<int>("observableCounter1", () => { counterState += 7; return counterState; }, "MB", "Size of universe",
1305-
new TagList() { { "ock1", "ocv1" }, { "ock2", "ocv2" }, { "ock3", "ocv3" } });
1306-
int gaugeState = 0;
1307-
ObservableGauge<int> og = meterA.CreateObservableGauge<int>("observableGauge1", () => { gaugeState += 9; return gaugeState; }, "12394923 asd [],;/", "junk!",
1308-
new TagList() { { "ogk1", "ogv1" } });
1309-
Histogram<int> h = meterB.CreateHistogram<int>("histogram1", "a unit", "the description", new TagList() { { "hk1", "hv1" }, { "hk2", "" }, {"hk3", null } });
1310-
UpDownCounter<int> udc = meterA.CreateUpDownCounter<int>("upDownCounter1", "udc unit", "udc description", new TagList() { { "udk1", "udv1" } });
1311-
int upDownCounterState = 0;
1312-
ObservableUpDownCounter<int> oudc = meterA.CreateObservableUpDownCounter<int>("observableUpDownCounter1", () => { upDownCounterState += 11; return upDownCounterState; }, "oudc unit", "oudc description");
1302+
object scope = new object();
13131303

1314-
EventWrittenEventArgs[] events;
1315-
using (MetricsEventListener listener = new MetricsEventListener(_output, MetricsEventListener.InstrumentPublishing, null, ""))
1316-
{
1317-
listener.WaitForEnumerationComplete(s_waitForEventTimeout);
1318-
events = listener.Events.ToArray();
1319-
}
1304+
using Meter meterA = new Meter("TestMeter10", null, null, scope);
1305+
using Meter meterB = new Meter("TestMeter11", null, new TagList() { { "Mk1", "Mv1" }, { "Mk2", null } }, scope);
1306+
Counter<int> c = meterA.CreateCounter<int>("counter1", "hat", "Fooz!!");
1307+
Gauge<int> g = meterA.CreateGauge<int>("gauge1", "C", "Temperature");
1308+
int counterState = 3;
1309+
ObservableCounter<int> oc = meterA.CreateObservableCounter<int>("observableCounter1", () => { counterState += 7; return counterState; }, "MB", "Size of universe",
1310+
new TagList() { { "ock1", "ocv1" }, { "ock2", "ocv2" }, { "ock3", "ocv3" } });
1311+
int gaugeState = 0;
1312+
ObservableGauge<int> og = meterA.CreateObservableGauge<int>("observableGauge1", () => { gaugeState += 9; return gaugeState; }, "12394923 asd [],;/", "junk!",
1313+
new TagList() { { "ogk1", "ogv1" } });
1314+
Histogram<int> h = meterB.CreateHistogram<int>("histogram1", "a unit", "the description", new TagList() { { "hk1", "hv1" }, { "hk2", "" }, {"hk3", null } });
1315+
UpDownCounter<int> udc = meterA.CreateUpDownCounter<int>("upDownCounter1", "udc unit", "udc description", new TagList() { { "udk1", "udv1" } });
1316+
int upDownCounterState = 0;
1317+
ObservableUpDownCounter<int> oudc = meterA.CreateObservableUpDownCounter<int>("observableUpDownCounter1", () => { upDownCounterState += 11; return upDownCounterState; }, "oudc unit", "oudc description");
13201318

1321-
AssertInstrumentPublishingEventsPresent(events, c, oc, og, h, udc, oudc, g);
1322-
AssertInitialEnumerationCompleteEventPresent(events);
1319+
EventWrittenEventArgs[] events;
1320+
using (MetricsEventListener listener = new MetricsEventListener(NullTestOutputHelper.Instance, MetricsEventListener.InstrumentPublishing, null, ""))
1321+
{
1322+
listener.WaitForEnumerationComplete(s_waitForEventTimeout);
1323+
events = listener.Events.ToArray();
1324+
}
1325+
1326+
AssertInstrumentPublishingEventsPresent(events, c, oc, og, h, udc, oudc, g);
1327+
AssertInitialEnumerationCompleteEventPresent(events);
1328+
}).Dispose();
13231329
}
13241330

13251331
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
@@ -1604,25 +1610,25 @@ public void EventSourceEnforcesHistogramLimitAndNotMaxTimeSeries()
16041610

16051611
public static IEnumerable<object[]> DifferentMetersAndInstrumentsData()
16061612
{
1607-
yield return new object[] { new Meter("M1").CreateCounter<int>("C1"), new Meter("M1").CreateCounter<int>("C1"), false};
1613+
yield return new object[] { new Meter("M1").CreateCounter<int>("C1"), new Meter("M2").CreateCounter<int>("C2"), false};
16081614

1609-
var counter = new Meter("M1").CreateCounter<int>("C1");
1610-
yield return new object[] { counter, counter.Meter.CreateCounter<int>("C1"), false };
1615+
var counter = new Meter("M2").CreateCounter<int>("C3");
1616+
yield return new object[] { counter, counter.Meter.CreateCounter<int>("C4"), false };
16111617

16121618
// Same counters
1613-
counter = new Meter("M1").CreateCounter<int>("C1");
1619+
counter = new Meter("M3").CreateCounter<int>("C5");
16141620
yield return new object[] { counter, counter, true };
16151621

16161622
var scope = new object();
16171623
yield return new object[]
16181624
{
1619-
new Meter("M1", "v1", new TagList { { "k1", "v1" } }, scope).CreateCounter<int>("C1", "u1", "d1", new TagList { { "k2", "v2" } } ),
1620-
new Meter("M1", "v1", new TagList { { "k1", "v1" } }, scope).CreateCounter<int>("C1", "u1", "d1", new TagList { { "k2", "v2" } } ),
1625+
new Meter("M4", "v1", new TagList { { "k1", "v1" } }, scope).CreateCounter<int>("C6", "u1", "d1", new TagList { { "k2", "v2" } } ),
1626+
new Meter("M5", "v1", new TagList { { "k1", "v1" } }, scope).CreateCounter<int>("C7", "u1", "d1", new TagList { { "k2", "v2" } } ),
16211627
false, // Same Instrument
16221628
};
16231629

1624-
Meter meter = new Meter("M1", "v1", new TagList { { "k1", "v1" } }, scope);
1625-
yield return new object[] { meter.CreateCounter<int>("C1", "u1", "d1", new TagList { { "k2", "v2" } } ), meter.CreateCounter<int>("C1", "u1", "d1", new TagList { { "k2", "v2" } } ), false };
1630+
Meter meter = new Meter("M6", "v1", new TagList { { "k1", "v1" } }, scope);
1631+
yield return new object[] { meter.CreateCounter<int>("C8", "u1", "d1", new TagList { { "k2", "v2" } } ), meter.CreateCounter<int>("C9", "u1", "d1", new TagList { { "k2", "v2" } } ), false };
16261632
}
16271633

16281634
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))]
@@ -1756,7 +1762,7 @@ private static void AssertHistogramLimitPresent(EventWrittenEventArgs[] events)
17561762

17571763
private static void AssertInstrumentPublishingEventsPresent(EventWrittenEventArgs[] events, params Instrument[] expectedInstruments)
17581764
{
1759-
var publishEvents = events.Where(e => e.EventName == "InstrumentPublished").Select(e =>
1765+
var publishEvents = events.Where(e => e.EventName == "InstrumentPublished" && e.Payload[1].ToString() != RuntimeMeterName).Select(e =>
17601766
new
17611767
{
17621768
MeterName = e.Payload[1].ToString(),

0 commit comments

Comments
 (0)