From 41fe826b109bbad61c1171b48f28086eb80d5bc3 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 13 May 2026 12:52:54 +1200 Subject: [PATCH] fix: prevent race condition when span count approaches the 1000-span limit Wraps AddChildSpan in a lock so concurrent span creation cannot exceed the limit or set IsSampled incorrectly under contention. Fixes #5173 Co-Authored-By: Claude Sonnet 4.6 --- src/Sentry/TransactionTracer.cs | 18 +++++++++++------- .../Protocol/SentryTransactionTests.cs | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/Sentry/TransactionTracer.cs b/src/Sentry/TransactionTracer.cs index e4ce907607..8bad607205 100644 --- a/src/Sentry/TransactionTracer.cs +++ b/src/Sentry/TransactionTracer.cs @@ -166,6 +166,7 @@ public IReadOnlyList Fingerprint public IReadOnlyDictionary Tags => _tags; private readonly ConcurrentBagLite _spans = new(); + private readonly Lock _childSpanLock = new(); /// public IReadOnlyCollection Spans => _spans; @@ -296,14 +297,17 @@ internal ISpan StartChild(SpanId? spanId, SpanId parentSpanId, string operation, private void AddChildSpan(SpanTracer span) { - // Limit spans to 1000 - var isOutOfLimit = _spans.Count >= 1000; - span.IsSampled = isOutOfLimit ? false : IsSampled; - - if (!isOutOfLimit) + lock (_childSpanLock) { - _spans.Add(span); - _activeSpanTracker.Push(span); + // Limit spans to 1000 + var isOutOfLimit = _spans.Count >= 1000; + span.IsSampled = isOutOfLimit ? false : IsSampled; + + if (!isOutOfLimit) + { + _spans.Add(span); + _activeSpanTracker.Push(span); + } } } diff --git a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs index 604e1cdda7..d8c2695385 100644 --- a/test/Sentry.Tests/Protocol/SentryTransactionTests.cs +++ b/test/Sentry.Tests/Protocol/SentryTransactionTests.cs @@ -378,6 +378,21 @@ public void StartChild_Limit_Maintained() spans.Count(s => s.IsSampled == true).Should().Be(1000); } + [Fact] + public void StartChild_Limit_Maintained_Concurrently() + { + // Arrange + var transaction = new TransactionTracer(DisabledHub.Instance, "my name", "my op"); + var spans = new ConcurrentBag(); + + // Act + Parallel.For(0, 5000, i => spans.Add(transaction.StartChild("span " + i))); + + // Assert + transaction.Spans.Should().HaveCount(1000); + spans.Count(s => s.IsSampled == true).Should().Be(1000); + } + [Fact] public void StartChild_SamplingInherited_True() {