Skip to content

Commit dcb8bd7

Browse files
authored
fix: memory leak when finishing unsampled Transactions/Spans (#4717)
1 parent 4089230 commit dcb8bd7

File tree

8 files changed

+147
-6
lines changed

8 files changed

+147
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
- The `Serilog` integration captures _Structured Logs_ (when enabled) independently of captured Events and added Breadcrumbs ([#4691](https://github.com/getsentry/sentry-dotnet/pull/4691))
2222
- Deliver system breadcrumbs in the main thread on Android ([#4671](https://github.com/getsentry/sentry-dotnet/pull/4671))
23+
- Memory leak when finishing an unsampled Transaction that has started unsampled Spans ([#4717](https://github.com/getsentry/sentry-dotnet/pull/4717))
2324

2425
## 6.0.0-preview.2
2526

src/Sentry/Internal/NoOpSpan.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,6 @@ public void SetMeasurement(string name, Measurement measurement)
8888

8989
public void Dispose()
9090
{
91+
Finish();
9192
}
9293
}

src/Sentry/Internal/UnsampledTransaction.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,20 @@ namespace Sentry.Internal;
44

55
/// <summary>
66
/// We know already, when starting a transaction, whether it's going to be sampled or not. When it's not sampled, we can
7-
/// avoid lots of unecessary processing. The only thing we need to track is the number of spans that would have been
7+
/// avoid lots of unnecessary processing. The only thing we need to track is the number of spans that would have been
88
/// created (the client reports detailing discarded events includes this detail).
99
/// </summary>
1010
internal sealed class UnsampledTransaction : NoOpTransaction
1111
{
1212
// Although it's a little bit wasteful to create separate individual class instances here when all we're going to
1313
// report to sentry is the span count (in the client report), SDK users may refer to things like
1414
// `ITransaction.Spans.Count`, so we create an actual collection
15+
#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
1516
private readonly ConcurrentBag<ISpan> _spans = [];
17+
#else
18+
private ConcurrentBag<ISpan> _spans = [];
19+
#endif
20+
1621
private readonly IHub _hub;
1722
private readonly ITransactionContext _context;
1823
private readonly SentryOptions? _options;
@@ -79,6 +84,9 @@ public override void Finish()
7984
_options?.ClientReportRecorder.RecordDiscardedEvent(discardReason, DataCategory.Span, spanCount);
8085

8186
_options?.LogDebug("Finished unsampled transaction");
87+
88+
// Release tracked spans
89+
ReleaseSpans();
8290
}
8391

8492
public override void Finish(SpanStatus status) => Finish();
@@ -103,4 +111,13 @@ public ISpan StartChild(string operation, SpanId spanId)
103111
_spans.Add(span);
104112
return span;
105113
}
114+
115+
private void ReleaseSpans()
116+
{
117+
#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
118+
_spans.Clear();
119+
#else
120+
_spans = [];
121+
#endif
122+
}
106123
}

src/Sentry/TransactionTracer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ public IReadOnlyList<string> Fingerprint
168168
/// <inheritdoc />
169169
public IReadOnlyDictionary<string, string> Tags => _tags;
170170

171-
#if NETSTANDARD2_1_OR_GREATER
171+
#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
172172
private readonly ConcurrentBag<ISpan> _spans = new();
173173
#else
174174
private ConcurrentBag<ISpan> _spans = new();
@@ -431,7 +431,7 @@ public string? Origin
431431

432432
private void ReleaseSpans()
433433
{
434-
#if NETSTANDARD2_1_OR_GREATER
434+
#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
435435
_spans.Clear();
436436
#else
437437
_spans = new ConcurrentBag<ISpan>();

test/Sentry.Tests/Internals/UnsampledSpanTests.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@ namespace Sentry.Tests.Internals;
22

33
public class UnsampledSpanTests
44
{
5+
[Fact]
6+
public void StartChild_IsUnsampledSpan_HasReferenceToUnsampledTransaction()
7+
{
8+
// Arrange
9+
var hub = Substitute.For<IHub>();
10+
ITransactionContext context = new TransactionContext("TestTransaction", "TestOperation",
11+
new SentryTraceHeader(SentryId.Create(), SpanId.Create(), false)
12+
);
13+
using var transaction = new UnsampledTransaction(hub, context);
14+
15+
// Act
16+
using var unsampledSpan = transaction.StartChild("Foo");
17+
18+
// Assert
19+
Assert.IsType<UnsampledSpan>(unsampledSpan);
20+
Assert.Same(transaction, unsampledSpan.GetTransaction());
21+
}
22+
523
[Fact]
624
public void GetTraceHeader_CreatesHeaderFromUnsampledTransaction()
725
{
@@ -10,8 +28,8 @@ public void GetTraceHeader_CreatesHeaderFromUnsampledTransaction()
1028
ITransactionContext context = new TransactionContext("TestTransaction", "TestOperation",
1129
new SentryTraceHeader(SentryId.Create(), SpanId.Create(), false)
1230
);
13-
var transaction = new UnsampledTransaction(hub, context);
14-
var unsampledSpan = transaction.StartChild("Foo");
31+
using var transaction = new UnsampledTransaction(hub, context);
32+
using var unsampledSpan = transaction.StartChild("Foo");
1533

1634
// Act
1735
var traceHeader = unsampledSpan.GetTraceHeader();
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
namespace Sentry.Tests.Internals;
2+
3+
public class UnsampledTransactionTests
4+
{
5+
[Fact]
6+
public void StartChild_CreatesSpan_IsTrackedByParent()
7+
{
8+
// Arrange
9+
var hub = Substitute.For<IHub>();
10+
ITransactionContext context = new TransactionContext("TestTransaction", "TestOperation",
11+
new SentryTraceHeader(SentryId.Create(), SpanId.Create(), false)
12+
);
13+
using var transaction = new UnsampledTransaction(hub, context);
14+
15+
// Act
16+
using var unsampledSpan = transaction.StartChild("Foo");
17+
18+
// Assert
19+
var span = Assert.Single(transaction.Spans);
20+
Assert.Same(unsampledSpan, span);
21+
}
22+
23+
[Fact]
24+
public void Finish_WithTrackedSpans_ClearsTrackedSpans()
25+
{
26+
// Arrange
27+
var hub = Substitute.For<IHub>();
28+
ITransactionContext context = new TransactionContext("TestTransaction", "TestOperation",
29+
new SentryTraceHeader(SentryId.Create(), SpanId.Create(), false)
30+
);
31+
var transaction = new UnsampledTransaction(hub, context);
32+
_ = transaction.StartChild("Foo");
33+
34+
// Act
35+
transaction.Finish();
36+
37+
// Assert
38+
Assert.Empty(transaction.Spans);
39+
}
40+
41+
[Fact]
42+
public void Dispose_WithTrackedSpans_ClearsTrackedSpans()
43+
{
44+
// Arrange
45+
var hub = Substitute.For<IHub>();
46+
ITransactionContext context = new TransactionContext("TestTransaction", "TestOperation",
47+
new SentryTraceHeader(SentryId.Create(), SpanId.Create(), false)
48+
);
49+
var transaction = new UnsampledTransaction(hub, context);
50+
_ = transaction.StartChild("Foo");
51+
52+
// Act
53+
transaction.Dispose();
54+
55+
// Assert
56+
Assert.Empty(transaction.Spans);
57+
}
58+
}

test/Sentry.Tests/SpanTracerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public void Dispose_IsUnfinished_Finishes()
5555
}
5656

5757
[Fact]
58-
public void Dispose_IsFinsished_DoesNothing()
58+
public void Dispose_IsFinished_DoesNothing()
5959
{
6060
// Arrange
6161
var hub = Substitute.For<IHub>();

test/Sentry.Tests/TransactionTracerTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,50 @@ public void Dispose_Finished_DoesNothing()
3030
// Assert
3131
hub.Received(1).CaptureTransaction(Arg.Is<SentryTransaction>(t => t.SpanId == transaction.SpanId));
3232
}
33+
34+
[Fact]
35+
public void StartChild_CreatesSpanTracer_TracksSpans()
36+
{
37+
// Arrange
38+
var hub = Substitute.For<IHub>();
39+
var transaction = new TransactionTracer(hub, "op", "name");
40+
41+
// Act
42+
var span = transaction.StartChild("operation");
43+
44+
// Assert
45+
Assert.IsType<SpanTracer>(span);
46+
Assert.Collection(transaction.Spans,
47+
element => Assert.Same(span, element));
48+
}
49+
50+
[Fact]
51+
public void Finish_WithTrackedSpans_ClearsTrackedSpans()
52+
{
53+
// Arrange
54+
var hub = Substitute.For<IHub>();
55+
var transaction = new TransactionTracer(hub, "op", "name");
56+
_ = transaction.StartChild("operation");
57+
58+
// Act
59+
transaction.Finish();
60+
61+
// Assert
62+
Assert.Empty(transaction.Spans);
63+
}
64+
65+
[Fact]
66+
public void Dispose_WithTrackedSpans_ClearsTrackedSpans()
67+
{
68+
// Arrange
69+
var hub = Substitute.For<IHub>();
70+
var transaction = new TransactionTracer(hub, "op", "name");
71+
_ = transaction.StartChild("operation");
72+
73+
// Act
74+
transaction.Dispose();
75+
76+
// Assert
77+
Assert.Empty(transaction.Spans);
78+
}
3379
}

0 commit comments

Comments
 (0)