Skip to content

Commit c2557af

Browse files
Added tests for FlushAsync and Dispose
1 parent 285a296 commit c2557af

File tree

2 files changed

+169
-25
lines changed

2 files changed

+169
-25
lines changed

src/Sentry/MetricAggregator.cs

+22-15
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ namespace Sentry;
77

88
internal class MetricAggregator : IMetricAggregator
99
{
10+
internal const string DisposingMessage = "Disposing MetricAggregator.";
11+
internal const string AlreadyDisposedMessage = "Already disposed MetricAggregator.";
12+
internal const string CancelledMessage = "Stopping the Metric Aggregator due to a cancellation.";
13+
internal const string ShutdownScheduledMessage = "Shutdown scheduled. Stopping by: {0}.";
14+
internal const string ShutdownImmediatelyMessage = "Exiting immediately due to 0 shutdown timeout.";
15+
internal const string FlushShutdownMessage = "Shutdown token triggered. Exiting metric aggregator.";
16+
1017
private readonly SentryOptions _options;
1118
private readonly IMetricHub _metricHub;
12-
private readonly TimeSpan _flushInterval;
1319

1420
private readonly SemaphoreSlim _codeLocationLock = new(1,1);
1521
private readonly ReaderWriterLockSlim _bucketsLock = new ReaderWriterLockSlim();
@@ -29,16 +35,14 @@ private readonly Lazy<Dictionary<long, ConcurrentDictionary<string, Metric>>> _b
2935
internal readonly ConcurrentDictionary<long, HashSet<MetricResourceIdentifier>> _seenLocations = new();
3036
internal Dictionary<long, Dictionary<MetricResourceIdentifier, SentryStackFrame>> _pendingLocations = new();
3137

32-
private readonly Task _loopTask;
38+
internal readonly Task _loopTask;
3339

3440
internal MetricAggregator(SentryOptions options, IMetricHub metricHub,
35-
CancellationTokenSource? shutdownSource = null,
36-
bool disableLoopTask = false, TimeSpan? flushInterval = null)
41+
CancellationTokenSource? shutdownSource = null, bool disableLoopTask = false)
3742
{
3843
_options = options;
3944
_metricHub = metricHub;
4045
_shutdownSource = shutdownSource ?? new CancellationTokenSource();
41-
_flushInterval = flushInterval ?? TimeSpan.FromSeconds(5);
4246

4347
if (disableLoopTask)
4448
{
@@ -305,12 +309,12 @@ private async Task RunLoopAsync()
305309
// If the cancellation was signaled, run until the end of the queue or shutdownTimeout
306310
try
307311
{
308-
await Task.Delay(_flushInterval, _shutdownSource.Token).ConfigureAwait(false);
312+
await Task.Delay(_options.ShutdownTimeout, _shutdownSource.Token).ConfigureAwait(false);
309313
}
310314
// Cancellation requested and no timeout allowed, so exit even if there are more items
311315
catch (OperationCanceledException) when (_options.ShutdownTimeout == TimeSpan.Zero)
312316
{
313-
_options.LogDebug("Exiting immediately due to 0 shutdown timeout.");
317+
_options.LogDebug(ShutdownImmediatelyMessage);
314318

315319
await shutdownTimeout.CancelAsync().ConfigureAwait(false);
316320

@@ -319,9 +323,7 @@ private async Task RunLoopAsync()
319323
// Cancellation requested, scheduled shutdown
320324
catch (OperationCanceledException)
321325
{
322-
_options.LogDebug(
323-
"Shutdown scheduled. Stopping by: {0}.",
324-
_options.ShutdownTimeout);
326+
_options.LogDebug(ShutdownScheduledMessage, _options.ShutdownTimeout);
325327

326328
shutdownTimeout.CancelAfterSafe(_options.ShutdownTimeout);
327329

@@ -391,15 +393,20 @@ public async Task FlushAsync(bool force = true, CancellationToken cancellationTo
391393
}
392394
catch (OperationCanceledException)
393395
{
394-
_options.LogInfo("Shutdown token triggered. Exiting metric aggregator.");
396+
_options.LogInfo(FlushShutdownMessage);
395397
}
396398
catch (Exception exception)
397399
{
398400
_options.LogError(exception, "Error processing metrics.");
399401
}
400402
finally
401403
{
402-
_flushLock.Release();
404+
// If the shutdown token was cancelled before we start this method, we can get here
405+
// without the _flushLock.CurrentCount (i.e. available threads) having been decremented
406+
if (_flushLock.CurrentCount < 1)
407+
{
408+
_flushLock.Release();
409+
}
403410
}
404411
}
405412

@@ -495,11 +502,11 @@ private void ClearStaleLocations()
495502
/// <inheritdoc cref="IAsyncDisposable.DisposeAsync"/>
496503
public async ValueTask DisposeAsync()
497504
{
498-
_options.LogDebug("Disposing MetricAggregator.");
505+
_options.LogDebug(DisposingMessage);
499506

500507
if (_disposed)
501508
{
502-
_options.LogDebug("Already disposed MetricAggregator.");
509+
_options.LogDebug(AlreadyDisposedMessage);
503510
return;
504511
}
505512

@@ -518,7 +525,7 @@ public async ValueTask DisposeAsync()
518525
}
519526
catch (OperationCanceledException)
520527
{
521-
_options.LogDebug("Stopping the Metric Aggregator due to a cancellation.");
528+
_options.LogDebug(CancelledMessage);
522529
}
523530
catch (Exception exception)
524531
{

test/Sentry.Tests/MetricAggregatorTests.cs

+147-10
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,33 @@ namespace Sentry.Tests;
44

55
public class MetricAggregatorTests
66
{
7-
class Fixture
7+
private class Fixture
88
{
9-
public SentryOptions Options { get; set; } = new();
10-
public IHub Hub { get; set; } = Substitute.For<IHub>();
11-
public IMetricHub MetricHub { get; set; } = Substitute.For<IMetricHub>();
12-
public bool DisableFlushLoop { get; set; } = true;
13-
public TimeSpan? FlushInterval { get; set; }
14-
public MetricAggregator GetSut()
15-
=> new(Options, MetricHub, disableLoopTask: DisableFlushLoop, flushInterval: FlushInterval);
9+
public readonly IDiagnosticLogger Logger;
10+
public readonly SentryOptions Options;
11+
12+
public readonly IMetricHub MetricHub;
13+
public bool DisableFlushLoop;
14+
public readonly CancellationTokenSource CancellationTokenSource;
15+
16+
public Fixture()
17+
{
18+
Logger = Substitute.For<IDiagnosticLogger>();
19+
Options = new SentryOptions
20+
{
21+
Debug = true,
22+
DiagnosticLogger = Logger
23+
};
24+
MetricHub = Substitute.For<IMetricHub>();
25+
DisableFlushLoop = true;
26+
CancellationTokenSource = new CancellationTokenSource();
27+
}
28+
29+
public MetricAggregator GetSut() => new(Options, MetricHub, CancellationTokenSource, DisableFlushLoop);
1630
}
1731

1832
// private readonly Fixture _fixture = new();
19-
private static readonly Fixture _fixture = new();
33+
private readonly Fixture _fixture = new();
2034

2135
[Fact]
2236
public void GetMetricBucketKey_GeneratesExpectedKey()
@@ -176,7 +190,8 @@ public async Task GetFlushableBuckets_IsThreadsafe()
176190
var sent = 0;
177191
MetricHelper.FlushShift = 0.0;
178192
_fixture.DisableFlushLoop = false;
179-
_fixture.FlushInterval = TimeSpan.FromMilliseconds(100);
193+
// TODO: Remove
194+
// _fixture.FlushInterval = TimeSpan.FromMilliseconds(100);
180195
_fixture.MetricHub.CaptureMetrics(Arg.Do<IEnumerable<Metric>>(metrics =>
181196
{
182197
foreach (var metric in metrics)
@@ -339,4 +354,126 @@ public void RecordCodeLocation_BadStackLevel_AddsToSeenButNotPending()
339354

340355
sut._pendingLocations.SelectMany(x => x.Value).Should().BeEmpty();
341356
}
357+
358+
[Fact]
359+
public void Dispose_OnlyExecutesOnce()
360+
{
361+
// Arrange
362+
_fixture.Logger.IsEnabled(Arg.Any<SentryLevel>()).Returns(true);
363+
var sut = _fixture.GetSut();
364+
365+
// Act
366+
sut.Dispose();
367+
sut.Dispose();
368+
sut.Dispose();
369+
370+
// Assert
371+
_fixture.Logger.Received(2).Log(SentryLevel.Debug, MetricAggregator.AlreadyDisposedMessage, null);
372+
}
373+
374+
[Fact]
375+
public void Dispose_StopsLoopTask()
376+
{
377+
// Arrange
378+
_fixture.Logger.IsEnabled(Arg.Any<SentryLevel>()).Returns(true);
379+
_fixture.DisableFlushLoop = false;
380+
_fixture.Options.ShutdownTimeout = TimeSpan.Zero;
381+
var sut = _fixture.GetSut();
382+
383+
// Act
384+
sut.Dispose();
385+
386+
// Assert
387+
_fixture.Logger.Received(1).Log(SentryLevel.Debug, MetricAggregator.DisposingMessage, null);
388+
sut._loopTask.Status.Should().BeOneOf(TaskStatus.RanToCompletion, TaskStatus.Faulted);
389+
}
390+
391+
[Fact]
392+
public async Task Dispose_SwallowsException()
393+
{
394+
// Arrange
395+
_fixture.CancellationTokenSource.Dispose();
396+
_fixture.DisableFlushLoop = false;
397+
var sut = _fixture.GetSut();
398+
399+
// We expect an exception here, because we disposed the cancellation token source
400+
await Assert.ThrowsAsync<ObjectDisposedException>(() => sut._loopTask);
401+
402+
// Act
403+
await sut.DisposeAsync();
404+
405+
// Assert
406+
sut._loopTask.Status.Should().Be(TaskStatus.Faulted);
407+
}
408+
409+
[Fact]
410+
public async Task Cancel_NonZeroTimeout_SchedulesShutdown()
411+
{
412+
// Arrange
413+
_fixture.Logger.IsEnabled(Arg.Any<SentryLevel>()).Returns(true);
414+
_fixture.DisableFlushLoop = false;
415+
_fixture.Options.ShutdownTimeout = TimeSpan.FromSeconds(1);
416+
var sut = _fixture.GetSut();
417+
418+
// Act
419+
await _fixture.CancellationTokenSource.CancelAsync();
420+
await Task.Delay(1000);
421+
422+
// Assert
423+
_fixture.Logger.Received(1).Log(SentryLevel.Debug, MetricAggregator.ShutdownScheduledMessage, null, Arg.Any<TimeSpan>());
424+
}
425+
426+
[Fact]
427+
public async Task Cancel_ZeroTimeout_ShutdownImmediately()
428+
{
429+
// Arrange
430+
_fixture.Logger.IsEnabled(Arg.Any<SentryLevel>()).Returns(true);
431+
_fixture.DisableFlushLoop = false;
432+
_fixture.Options.ShutdownTimeout = TimeSpan.Zero;
433+
var sut = _fixture.GetSut();
434+
435+
// Act
436+
await _fixture.CancellationTokenSource.CancelAsync();
437+
await Task.Delay(1000);
438+
439+
// Assert
440+
_fixture.Logger.Received(1).Log(SentryLevel.Debug, MetricAggregator.ShutdownImmediatelyMessage, null);
441+
}
442+
443+
[Fact]
444+
public async Task FlushAsync_FlushesPendingLocations()
445+
{
446+
// Arrange
447+
var type = MetricType.Counter;
448+
var key = "counter_key";
449+
var unit = MeasurementUnit.None;
450+
var stackLevel = 1;
451+
var timestamp = DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(20));
452+
var sut = _fixture.GetSut();
453+
sut.RecordCodeLocation(type, key, unit, stackLevel, timestamp);
454+
455+
// Act
456+
await sut.FlushAsync();
457+
458+
// Assert
459+
_fixture.MetricHub.Received(1).CaptureCodeLocations(Arg.Any<CodeLocations>());
460+
}
461+
462+
[Fact]
463+
public async Task FlushAsync_Cancel_Exists()
464+
{
465+
// Arrange
466+
_fixture.DisableFlushLoop = false;
467+
_fixture.Logger.IsEnabled(Arg.Any<SentryLevel>()).Returns(true);
468+
var cancellationTokenSource = new CancellationTokenSource();
469+
await cancellationTokenSource.CancelAsync();
470+
var sut = _fixture.GetSut();
471+
472+
// Act
473+
await sut.FlushAsync(true, cancellationTokenSource.Token);
474+
// await Task.Delay(1000);
475+
476+
// Assert
477+
_fixture.Logger.Received(1).Log(SentryLevel.Info, MetricAggregator.FlushShutdownMessage, null);
478+
}
342479
}

0 commit comments

Comments
 (0)