Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions src/Sentry.OpenTelemetry/SentrySpanProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,15 @@ private void CreateChildSpan(Activity data, ISpan parentSpan, SpanId? parentSpan
};

var span = parentSpan.StartChild(context);
// Used to filter out spans that are not recorded when finishing a transaction
span.SetFused(data);
// Fuse the Activity via a WeakReference so _map does not pin it, letting PruneFilteredSpans evict
// never-ended spans. #3198 intended weak refs here but SetFused stores the value strongly.
SetFusedActivity(span, data);
if (span is SpanTracer spanTracer)
{
spanTracer.Origin = OpenTelemetryOrigin;
spanTracer.StartTimestamp = data.StartTimeUtc;
spanTracer.IsFiltered = () => spanTracer.GetFused<Activity>() is { IsAllDataRequested: false, Recorded: false };
// Used to filter out spans that are not recorded when finishing a transaction.
spanTracer.IsFiltered = () => GetFusedActivity(spanTracer) is { IsAllDataRequested: false, Recorded: false };
}
_map[data.SpanId] = span;
}
Expand Down Expand Up @@ -176,7 +178,8 @@ private void CreateRootSpan(Activity data)
{
scope.Transaction ??= transaction;
}, transaction);
transaction.SetFused(data);
// Fuse weakly so _map does not pin the Activity.
SetFusedActivity(transaction, data);
_map[data.SpanId] = transaction;
}

Expand Down Expand Up @@ -304,16 +307,28 @@ internal void PruneFilteredSpans(bool force = false)
foreach (var mappedItem in _map)
{
var (spanId, span) = mappedItem;
var activity = span.GetFused<Activity>();
// Also prune when the activity has been GC'd (weak ref returns null): the activity is gone, so it
// can never call OnEnd, and the span will never be removed otherwise — causing a memory leak.
var activity = GetFusedActivity(span);
// Prune when the activity has been GC'd (weak ref returns null): it can no longer call OnEnd, so
// the span would otherwise stay in _map forever.
if (activity is null or { Recorded: false, IsAllDataRequested: false })
{
_map.TryRemove(spanId, out _);
}
}
}

private const string ActivityPropertyName = "Activity";

// Fuses the Activity onto the span via a WeakReference so _map does not pin it.
private static void SetFusedActivity(ISpan span, Activity data) =>
span.SetFused(ActivityPropertyName, new WeakReference<Activity>(data));

// Returns the fused Activity, or null once it has been garbage-collected.
private static Activity? GetFusedActivity(ISpan span) =>
span.GetFused<WeakReference<Activity>>(ActivityPropertyName) is { } weakRef && weakRef.TryGetTarget(out var activity)
? activity
: null;

private bool NeedsPruning()
{
var lastPruned = Interlocked.Read(ref _lastPruned);
Expand Down
19 changes: 19 additions & 0 deletions test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,25 @@ public void PruneFilteredSpans_GarbageCollectedActivity_Pruned()
Assert.False(sut._map.TryGetValue(spanId, out _));
}

[Fact]
public void OnStart_FusesActivityWeakly()
{
// Arrange
_fixture.Options.Instrumenter = Instrumenter.OpenTelemetry;
var sut = _fixture.GetSut();

using var activity = Tracer.StartActivity("test")!;

// Act
sut.OnStart(activity);

// The Activity must be fused via a WeakReference, not strongly. A strong reference is pinned by
// _map, which prevents GC and defeats PruneFilteredSpans, leaking never-ended spans.
sut._map.TryGetValue(activity.SpanId, out var span).Should().BeTrue();
span.GetFused<WeakReference<Activity>>("Activity").Should().NotBeNull();
span.GetFused<Activity>("Activity").Should().BeNull("the Activity must not be fused with a strong reference");
}

[Fact]
public void PruneFilteredSpans_RecentlyPruned_DoesNothing()
{
Expand Down
Loading