Skip to content

Commit 49fefc1

Browse files
committed
ref: discard logs gracefully when Hub is (being) disposed
1 parent d72ca5c commit 49fefc1

File tree

11 files changed

+261
-55
lines changed

11 files changed

+261
-55
lines changed

benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.StructuredLogBatchProcessorBenchmarks-report-github.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ Apple M3 Pro, 1 CPU, 12 logical and 12 physical cores
88
99
1010
```
11-
| Method | BatchCount | OperationsPerInvoke | Mean | Error | StdDev | Gen0 | Allocated |
12-
|---------------- |----------- |-------------------- |------------:|----------:|----------:|-------:|----------:|
13-
| **EnqueueAndFlush** | **10** | **100** | **1,874.9 ns** | **33.18 ns** | **31.04 ns** | **0.6104** | **5 KB** |
14-
| **EnqueueAndFlush** | **10** | **200** | **3,770.1 ns** | **55.49 ns** | **51.91 ns** | **1.2207** | **10 KB** |
15-
| **EnqueueAndFlush** | **10** | **1000** | **17,993.8 ns** | **359.21 ns** | **467.07 ns** | **6.1035** | **50 KB** |
16-
| **EnqueueAndFlush** | **100** | **100** | **809.3 ns** | **15.05 ns** | **15.45 ns** | **0.1469** | **1.2 KB** |
17-
| **EnqueueAndFlush** | **100** | **200** | **1,551.0 ns** | **16.17 ns** | **14.33 ns** | **0.2937** | **2.41 KB** |
18-
| **EnqueueAndFlush** | **100** | **1000** | **7,782.0 ns** | **123.71 ns** | **109.67 ns** | **1.4648** | **12.03 KB** |
11+
| Method | BatchCount | OperationsPerInvoke | Mean | Error | StdDev | Gen0 | Allocated |
12+
|---------------- |----------- |-------------------- |------------:|---------:|---------:|-------:|----------:|
13+
| **EnqueueAndFlush** | **10** | **100** | **1,774.5 ns** | **7.57 ns** | **6.71 ns** | **0.6104** | **5 KB** |
14+
| **EnqueueAndFlush** | **10** | **200** | **3,468.5 ns** | **11.16 ns** | **10.44 ns** | **1.2207** | **10 KB** |
15+
| **EnqueueAndFlush** | **10** | **1000** | **17,259.7 ns** | **51.92 ns** | **46.02 ns** | **6.1035** | **50 KB** |
16+
| **EnqueueAndFlush** | **100** | **100** | **857.5 ns** | **4.21 ns** | **3.73 ns** | **0.1469** | **1.2 KB** |
17+
| **EnqueueAndFlush** | **100** | **200** | **1,681.4 ns** | **1.74 ns** | **1.63 ns** | **0.2937** | **2.41 KB** |
18+
| **EnqueueAndFlush** | **100** | **1000** | **8,302.2 ns** | **12.00 ns** | **10.64 ns** | **1.4648** | **12.03 KB** |

benchmarks/Sentry.Benchmarks/StructuredLogBatchProcessorBenchmarks.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace Sentry.Benchmarks;
77

88
public class StructuredLogBatchProcessorBenchmarks
99
{
10+
private Hub _hub;
1011
private StructuredLogBatchProcessor _batchProcessor;
1112
private SentryLog _log;
1213

@@ -19,12 +20,32 @@ public class StructuredLogBatchProcessorBenchmarks
1920
[GlobalSetup]
2021
public void Setup()
2122
{
22-
var hub = DisabledHub.Instance;
23+
SentryOptions options = new()
24+
{
25+
Dsn = DsnSamples.ValidDsn,
26+
Experimental =
27+
{
28+
EnableLogs = true,
29+
},
30+
};
31+
2332
var batchInterval = Timeout.InfiniteTimeSpan;
33+
2434
var clientReportRecorder = Substitute.For<IClientReportRecorder>();
35+
clientReportRecorder
36+
.When(static recorder => recorder.RecordDiscardedEvent(Arg.Any<DiscardReason>(), Arg.Any<DataCategory>(), Arg.Any<int>()))
37+
.Throw<UnreachableException>();
38+
2539
var diagnosticLogger = Substitute.For<IDiagnosticLogger>();
26-
_batchProcessor = new StructuredLogBatchProcessor(hub, BatchCount, batchInterval, clientReportRecorder, diagnosticLogger);
40+
diagnosticLogger
41+
.When(static logger => logger.IsEnabled(Arg.Any<SentryLevel>()))
42+
.Throw<UnreachableException>();
43+
diagnosticLogger
44+
.When(static logger => logger.Log(Arg.Any<SentryLevel>(), Arg.Any<string>(), Arg.Any<Exception>(), Arg.Any<object[]>()))
45+
.Throw<UnreachableException>();
2746

47+
_hub = new Hub(options, DisabledHub.Instance);
48+
_batchProcessor = new StructuredLogBatchProcessor(_hub, BatchCount, batchInterval, clientReportRecorder, diagnosticLogger);
2849
_log = new SentryLog(DateTimeOffset.Now, SentryId.Empty, SentryLogLevel.Trace, "message");
2950
}
3051

@@ -42,5 +63,6 @@ public void EnqueueAndFlush()
4263
public void Cleanup()
4364
{
4465
_batchProcessor.Dispose();
66+
_hub.Dispose();
4567
}
4668
}

src/Sentry/Internal/DefaultSentryStructuredLogger.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal sealed class DefaultSentryStructuredLogger : SentryStructuredLogger
1313

1414
internal DefaultSentryStructuredLogger(IHub hub, SentryOptions options, ISystemClock clock, int batchCount, TimeSpan batchInterval)
1515
{
16+
Debug.Assert(hub.IsEnabled);
1617
Debug.Assert(options is { Experimental.EnableLogs: true });
1718

1819
_hub = hub;

src/Sentry/Internal/StructuredLogBatchBuffer.cs

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Sentry.Internal;
99
/// Must be attempted to flush via <see cref="TryEnterFlushScope"/> when either the <see cref="Capacity"/> is reached,
1010
/// or when the <see cref="_timeout"/> is exceeded.
1111
/// </remarks>
12-
[DebuggerDisplay("Name = {Name}, Capacity = {Capacity}, Additions = {_additions}, AddCount = {AddCount}")]
12+
[DebuggerDisplay("Name = {Name}, Capacity = {Capacity}, Additions = {_additions}, AddCount = {AddCount}, IsDisposed = {_disposed}")]
1313
internal sealed class StructuredLogBatchBuffer : IDisposable
1414
{
1515
private readonly SentryLog[] _array;
@@ -18,9 +18,10 @@ internal sealed class StructuredLogBatchBuffer : IDisposable
1818

1919
private readonly Timer _timer;
2020
private readonly TimeSpan _timeout;
21-
2221
private readonly Action<StructuredLogBatchBuffer> _timeoutExceededAction;
2322

23+
private volatile bool _disposed;
24+
2425
/// <summary>
2526
/// Create a new buffer.
2627
/// </summary>
@@ -39,8 +40,8 @@ public StructuredLogBatchBuffer(int capacity, TimeSpan timeout, Action<Structure
3940

4041
_timer = new Timer(OnIntervalElapsed, this, Timeout.Infinite, Timeout.Infinite);
4142
_timeout = timeout;
42-
4343
_timeoutExceededAction = timeoutExceededAction;
44+
4445
Name = name ?? "default";
4546
}
4647

@@ -62,22 +63,25 @@ public StructuredLogBatchBuffer(int capacity, TimeSpan timeout, Action<Structure
6263
/// <summary>
6364
/// Number of <see cref="Add"/> operations in progress.
6465
/// </summary>
65-
/// <remarks>
66-
/// This property is used for debugging only.
67-
/// </remarks>
6866
private int AddCount => _addLock.Count;
6967

7068
/// <summary>
71-
/// Attempt to atomically add one element to the buffer.
69+
/// Attempt to add one element to the buffer.
70+
/// Is thread-safe.
7271
/// </summary>
73-
/// <param name="item">Element attempted to be added atomically.</param>
74-
/// <returns>An <see cref="BatchBufferAddStatus"/> describing the result of the operation.</returns>
75-
internal BatchBufferAddStatus Add(SentryLog item)
72+
/// <param name="item">Element attempted to be added.</param>
73+
/// <returns>An <see cref="StructuredLogBatchBufferAddStatus"/> describing the result of the thread-safe operation.</returns>
74+
internal StructuredLogBatchBufferAddStatus Add(SentryLog item)
7675
{
76+
if (_disposed)
77+
{
78+
return StructuredLogBatchBufferAddStatus.IgnoredIsDisposed;
79+
}
80+
7781
using var scope = _addLock.TryEnterCounterScope();
7882
if (!scope.IsEntered)
7983
{
80-
return BatchBufferAddStatus.IgnoredIsFlushing;
84+
return StructuredLogBatchBufferAddStatus.IgnoredIsFlushing;
8185
}
8286

8387
var count = Interlocked.Increment(ref _additions);
@@ -86,24 +90,24 @@ internal BatchBufferAddStatus Add(SentryLog item)
8690
{
8791
EnableTimer();
8892
_array[count - 1] = item;
89-
return BatchBufferAddStatus.AddedFirst;
93+
return StructuredLogBatchBufferAddStatus.AddedFirst;
9094
}
9195

9296
if (count < _array.Length)
9397
{
9498
_array[count - 1] = item;
95-
return BatchBufferAddStatus.Added;
99+
return StructuredLogBatchBufferAddStatus.Added;
96100
}
97101

98102
if (count == _array.Length)
99103
{
100104
DisableTimer();
101105
_array[count - 1] = item;
102-
return BatchBufferAddStatus.AddedLast;
106+
return StructuredLogBatchBufferAddStatus.AddedLast;
103107
}
104108

105109
Debug.Assert(count > _array.Length);
106-
return BatchBufferAddStatus.IgnoredCapacityExceeded;
110+
return StructuredLogBatchBufferAddStatus.IgnoredCapacityExceeded;
107111
}
108112

109113
/// <summary>
@@ -114,6 +118,11 @@ internal BatchBufferAddStatus Add(SentryLog item)
114118
/// </remarks>
115119
internal FlushScope TryEnterFlushScope()
116120
{
121+
if (_disposed)
122+
{
123+
return new FlushScope();
124+
}
125+
117126
var scope = _addLock.TryEnterLockScope();
118127
if (scope.IsEntered)
119128
{
@@ -132,11 +141,14 @@ private void ExitFlushScope()
132141
}
133142

134143
/// <summary>
135-
/// Callback when Timer has elapsed after first item has been added.
144+
/// Callback when Timer has elapsed after first item has been added and buffer is not full yet.
136145
/// </summary>
137146
internal void OnIntervalElapsed(object? state)
138147
{
139-
_timeoutExceededAction(this);
148+
if (!_disposed)
149+
{
150+
_timeoutExceededAction(this);
151+
}
140152
}
141153

142154
/// <summary>
@@ -193,21 +205,20 @@ private void Clear(int length)
193205

194206
private void EnableTimer()
195207
{
196-
var updated = _timer.Change(_timeout, Timeout.InfiniteTimeSpan);
197-
Debug.Assert(updated, "Timer was not successfully enabled.");
208+
_ = _timer.Change(_timeout, Timeout.InfiniteTimeSpan);
198209
}
199210

200211
private void DisableTimer()
201212
{
202-
var updated = _timer.Change(Timeout.Infinite, Timeout.Infinite);
203-
Debug.Assert(updated, "Timer was not successfully disabled.");
213+
_ = _timer.Change(Timeout.Infinite, Timeout.Infinite);
204214
}
205215

206216
/// <inheritdoc />
207217
public void Dispose()
208218
{
209219
_timer.Dispose();
210220
_addLock.Dispose();
221+
_disposed = true;
211222
}
212223

213224
private static void ThrowIfLessThanTwo(int value, string paramName)
@@ -240,7 +251,7 @@ static void ThrowNegativeOrZero(TimeSpan value, string paramName)
240251
/// A scope than ensures only a single <see cref="Flush"/> operation is in progress,
241252
/// and blocks the calling thread until all <see cref="Add"/> operations have finished.
242253
/// When <see cref="IsEntered"/> is <see langword="true"/>, no more <see cref="Add"/> can be started,
243-
/// which will then return <see cref="BatchBufferAddStatus.IgnoredIsFlushing"/> immediately.
254+
/// which will then return <see cref="StructuredLogBatchBufferAddStatus.IgnoredIsFlushing"/> immediately.
244255
/// </summary>
245256
/// <remarks>
246257
/// Only <see cref="Flush"/> when scope <see cref="IsEntered"/>.
@@ -287,11 +298,12 @@ public void Dispose()
287298
}
288299
}
289300

290-
internal enum BatchBufferAddStatus : byte
301+
internal enum StructuredLogBatchBufferAddStatus : byte
291302
{
292303
AddedFirst,
293304
Added,
294305
AddedLast,
295306
IgnoredCapacityExceeded,
296307
IgnoredIsFlushing,
308+
IgnoredIsDisposed,
297309
}

src/Sentry/Internal/StructuredLogBatchProcessor.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using Sentry.Extensibility;
2-
using Sentry.Infrastructure;
32
using Sentry.Protocol;
43
using Sentry.Protocol.Envelopes;
54

@@ -9,11 +8,14 @@ namespace Sentry.Internal;
98
/// The Batch Processor for Sentry Logs.
109
/// </summary>
1110
/// <remarks>
12-
/// Used a double buffer strategy to achieve synchronous and lock-free adding.
11+
/// Uses a double buffer strategy to achieve synchronous and lock-free adding.
1312
/// Switches the active buffer either when full or timeout exceeded (after first item added).
1413
/// Logs are dropped when both buffers are either full or being flushed.
14+
/// Logs are not enqueued when the Hub is disabled (Hub is being or has been disposed).
1515
/// Flushing blocks the calling thread until all pending add operations have completed.
1616
/// <code>
17+
/// Implementation:
18+
/// - When Hub is disabled (i.e. disposed), does not enqueue log
1719
/// - Try to enqueue log into currently active buffer
1820
/// - when currently active buffer is full, try to enqueue log into the other buffer
1921
/// - when the other buffer is also full, or currently being flushed, then the log is dropped and a discarded event is recorded as a client report
@@ -52,6 +54,11 @@ public StructuredLogBatchProcessor(IHub hub, int batchCount, TimeSpan batchInter
5254

5355
internal void Enqueue(SentryLog log)
5456
{
57+
if (!_hub.IsEnabled)
58+
{
59+
return;
60+
}
61+
5562
var activeBuffer = _activeBuffer;
5663

5764
if (!TryEnqueue(activeBuffer, log))
@@ -74,6 +81,9 @@ internal void Flush()
7481
/// <summary>
7582
/// Forces invocation of a Timeout of the active buffer.
7683
/// </summary>
84+
/// <remarks>
85+
/// Intended for Testing only.
86+
/// </remarks>
7787
internal void OnIntervalElapsed()
7888
{
7989
var activeBuffer = _activeBuffer;
@@ -84,14 +94,14 @@ private bool TryEnqueue(StructuredLogBatchBuffer buffer, SentryLog log)
8494
{
8595
var status = buffer.Add(log);
8696

87-
if (status is BatchBufferAddStatus.AddedLast)
97+
if (status is StructuredLogBatchBufferAddStatus.AddedLast)
8898
{
8999
SwapActiveBuffer(buffer);
90100
CaptureLogs(buffer);
91101
return true;
92102
}
93103

94-
return status is BatchBufferAddStatus.AddedFirst or BatchBufferAddStatus.Added;
104+
return status is StructuredLogBatchBufferAddStatus.AddedFirst or StructuredLogBatchBufferAddStatus.Added;
95105
}
96106

97107
private void SwapActiveBuffer(StructuredLogBatchBuffer currentActiveBuffer)

src/Sentry/Threading/ScopedCountdownLock.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ internal ScopedCountdownLock()
4444
/// Check via <see cref="CounterScope.IsEntered"/> whether the underlying <see cref="CountdownEvent"/> has not been set/signaled yet.
4545
/// To signal the underlying <see cref="CountdownEvent"/>, ensure <see cref="CounterScope.Dispose"/> is called.
4646
/// </summary>
47+
/// <remarks>
48+
/// Must be disposed to exit.
49+
/// </remarks>
4750
internal CounterScope TryEnterCounterScope()
4851
{
4952
if (_event.TryAddCount(1))
@@ -60,11 +63,14 @@ private void ExitCounterScope()
6063
}
6164

6265
/// <summary>
63-
/// When successful, the lock <see cref="IsEngaged"/>, <see cref="Count"/> can reach <see langword="0"/> when no <see cref="CounterScope"/> is active, and the event be set/signaled.
66+
/// When successful, the lock <see cref="IsEngaged"/>, <see cref="Count"/> can reach <see langword="0"/> when no <see cref="CounterScope"/> is active, and the event can be set/signaled.
6467
/// Check via <see cref="LockScope.IsEntered"/> whether the Lock <see cref="IsEngaged"/>.
6568
/// Use <see cref="LockScope.Wait"/> to block until every active <see cref="CounterScope"/> has exited before performing the locked operation.
6669
/// After the locked operation has completed, disengage the Lock via <see cref="LockScope.Dispose"/>.
6770
/// </summary>
71+
/// <remarks>
72+
/// Must be disposed to exit.
73+
/// </remarks>
6874
internal LockScope TryEnterLockScope()
6975
{
7076
if (Interlocked.CompareExchange(ref _isEngaged, 1, 0) == 0)

test/Sentry.Testing/InMemoryDiagnosticLogger.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ public void Log(SentryLevel logLevel, string message, Exception exception = null
1111
Entries.Enqueue(new Entry(logLevel, message, exception, args));
1212
}
1313

14+
internal Entry Dequeue()
15+
{
16+
if (Entries.TryDequeue(out var entry))
17+
{
18+
return entry;
19+
}
20+
21+
throw new InvalidOperationException("Queue is empty.");
22+
}
23+
1424
public record Entry(
1525
SentryLevel Level,
1626
string Message,

0 commit comments

Comments
 (0)