diff --git a/src/OpenTelemetry/Trace/Export/BatchingSpanProcessor.cs b/src/OpenTelemetry/Trace/Export/BatchingSpanProcessor.cs index f173238af6a..88477a84dd2 100644 --- a/src/OpenTelemetry/Trace/Export/BatchingSpanProcessor.cs +++ b/src/OpenTelemetry/Trace/Export/BatchingSpanProcessor.cs @@ -38,7 +38,7 @@ public class BatchingSpanProcessor : SpanProcessor, IDisposable private readonly TimeSpan scheduleDelay; private readonly Task worker; private CancellationTokenSource cts; - private int currentQueueSize; + private volatile int currentQueueSize; private bool stopping = false; /// <summary> @@ -113,6 +113,7 @@ public override void OnEnd(Span span) } Interlocked.Increment(ref this.currentQueueSize); + this.exportQueue.Enqueue(span); } @@ -130,9 +131,7 @@ public override async Task ShutdownAsync(CancellationToken cancellationToken) // if there are more items, continue until cancellation token allows while (this.currentQueueSize > 0 && !cancellationToken.IsCancellationRequested) { - Debug.WriteLine($"!!! {DateTime.UtcNow:o} export " + this.currentQueueSize); await this.ExportBatchAsync(cancellationToken).ConfigureAwait(false); - Debug.WriteLine($"!!! {DateTime.UtcNow:o} export finished" + this.currentQueueSize); } // there is no point in waiting for a worker task if cancellation happens @@ -161,16 +160,19 @@ private async Task ExportBatchAsync(CancellationToken cancellationToken) return; } - var nextBatchSize = Math.Min(this.currentQueueSize, this.maxExportBatchSize); - - if (nextBatchSize == 0) + List<Span> batch = null; + if (this.exportQueue.TryDequeue(out var nextSpan)) + { + Interlocked.Decrement(ref this.currentQueueSize); + batch = new List<Span> { nextSpan }; + } + else { + // nothing in queue return; } - var batch = new List<Span>(nextBatchSize); - - while (batch.Count < nextBatchSize && this.exportQueue.TryDequeue(out var nextSpan)) + while (batch.Count < this.maxExportBatchSize && this.exportQueue.TryDequeue(out nextSpan)) { Interlocked.Decrement(ref this.currentQueueSize); batch.Add(nextSpan); diff --git a/test/OpenTelemetry.Tests/Impl/Testing/Export/TestExporter.cs b/test/OpenTelemetry.Tests/Impl/Testing/Export/TestExporter.cs index e76cc546c91..fd41537dab7 100644 --- a/test/OpenTelemetry.Tests/Impl/Testing/Export/TestExporter.cs +++ b/test/OpenTelemetry.Tests/Impl/Testing/Export/TestExporter.cs @@ -14,12 +14,11 @@ // limitations under the License. // </copyright> -using System.Collections.Concurrent; - namespace OpenTelemetry.Testing.Export { using System; using System.Collections.Generic; + using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using OpenTelemetry.Trace; diff --git a/test/OpenTelemetry.Tests/Impl/Trace/Export/BatchingSpanProcessorTests.cs b/test/OpenTelemetry.Tests/Impl/Trace/Export/BatchingSpanProcessorTests.cs index b812bb81fab..ed4330c031c 100644 --- a/test/OpenTelemetry.Tests/Impl/Trace/Export/BatchingSpanProcessorTests.cs +++ b/test/OpenTelemetry.Tests/Impl/Trace/Export/BatchingSpanProcessorTests.cs @@ -36,16 +36,10 @@ public class BatchingSpanProcessorTest : IDisposable private const string SpanName1 = "MySpanName/1"; private const string SpanName2 = "MySpanName/2"; - private TestExporter spanExporter = new TestExporter(null); - private BatchingSpanProcessor spanProcessor; private static readonly TimeSpan DefaultDelay = TimeSpan.FromMilliseconds(30); private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(1); - public BatchingSpanProcessorTest() - { - spanProcessor = new BatchingSpanProcessor(spanExporter, 128, DefaultDelay, 128); - } - private Span CreateSampledEndedSpan(string spanName) + private Span CreateSampledEndedSpan(string spanName, SpanProcessor spanProcessor) { var sampledActivity = new Activity(spanName); sampledActivity.ActivityTraceFlags |= ActivityTraceFlags.Recorded; @@ -65,7 +59,7 @@ private Span CreateSampledEndedSpan(string spanName) return span; } - private Span CreateNotSampledEndedSpan(string spanName) + private Span CreateNotSampledEndedSpan(string spanName, SpanProcessor spanProcessor) { var notSampledActivity = new Activity(spanName); notSampledActivity.SetIdFormat(ActivityIdFormat.W3C); @@ -96,40 +90,50 @@ public void ThrowsOnInvalidArguments() [Fact] public async Task ShutdownTwice() { - spanProcessor = new BatchingSpanProcessor(new NoopSpanExporter()); + using (var spanProcessor = new BatchingSpanProcessor(new NoopSpanExporter())) + { - await spanProcessor.ShutdownAsync(CancellationToken.None); + await spanProcessor.ShutdownAsync(CancellationToken.None); - // does not throw - await spanProcessor.ShutdownAsync(CancellationToken.None); + // does not throw + await spanProcessor.ShutdownAsync(CancellationToken.None); + } } [Fact] public async Task ShutdownWithHugeScheduleDelay() { - spanProcessor = new BatchingSpanProcessor(new NoopSpanExporter(), 128, TimeSpan.FromMinutes(1), 32); - - var sw = Stopwatch.StartNew(); - using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100))) + using (var spanProcessor = + new BatchingSpanProcessor(new NoopSpanExporter(), 128, TimeSpan.FromMinutes(1), 32)) { - cts.Token.ThrowIfCancellationRequested(); - await spanProcessor.ShutdownAsync(cts.Token).ConfigureAwait(false); + + var sw = Stopwatch.StartNew(); + using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100))) + { + cts.Token.ThrowIfCancellationRequested(); + await spanProcessor.ShutdownAsync(cts.Token).ConfigureAwait(false); + } + + sw.Stop(); + Assert.InRange(sw.Elapsed, TimeSpan.Zero, TimeSpan.FromMilliseconds(100)); } - sw.Stop(); - Assert.InRange(sw.Elapsed, TimeSpan.Zero, TimeSpan.FromMilliseconds(100)); } [Fact] public void ExportDifferentSampledSpans() { - var span1 = CreateSampledEndedSpan(SpanName1); - var span2 = CreateSampledEndedSpan(SpanName2); + var spanExporter = new TestExporter(null); + using (var spanProcessor = new BatchingSpanProcessor(spanExporter, 128, DefaultDelay, 128)) + { + var span1 = CreateSampledEndedSpan(SpanName1, spanProcessor); + var span2 = CreateSampledEndedSpan(SpanName2, spanProcessor); - var exported = WaitForSpans(spanExporter, 2, DefaultTimeout); + var exported = WaitForSpans(spanExporter, 2, DefaultTimeout); - Assert.Equal(2, exported.Length); - Assert.Contains(span1, exported); - Assert.Contains(span2, exported); + Assert.Equal(2, exported.Length); + Assert.Contains(span1, exported); + Assert.Contains(span2, exported); + } } [Fact] @@ -137,28 +141,30 @@ public void ExporterIsSlowerThanDelay() { var exportStartTimes = new List<long>(); var exportEndTimes = new List<long>(); - spanExporter = new TestExporter(_ => + var spanExporter = new TestExporter(_ => { exportStartTimes.Add(Stopwatch.GetTimestamp()); Thread.Sleep(50); exportEndTimes.Add(Stopwatch.GetTimestamp()); }); - spanProcessor = new BatchingSpanProcessor(spanExporter, 128, TimeSpan.FromMilliseconds(30), 2); - var spans = new List<Span>(); - for (int i = 0; i < 20; i++) + using (var spanProcessor = new BatchingSpanProcessor(spanExporter, 128, TimeSpan.FromMilliseconds(30), 2)) { - spans.Add(CreateSampledEndedSpan(i.ToString())); - } + var spans = new List<Span>(); + for (int i = 0; i < 20; i++) + { + spans.Add(CreateSampledEndedSpan(i.ToString(), spanProcessor)); + } - var exported = WaitForSpans(spanExporter, 20, TimeSpan.FromSeconds(2)); + var exported = WaitForSpans(spanExporter, 20, TimeSpan.FromSeconds(2)); - Assert.Equal(spans.Count, exported.Length); - Assert.InRange(exportStartTimes.Count, 10, 20); + Assert.Equal(spans.Count, exported.Length); + Assert.InRange(exportStartTimes.Count, 10, 20); - for (int i = 1; i < exportStartTimes.Count - 1; i ++) - { - Assert.InRange(exportStartTimes[i], exportEndTimes[i - 1] + 1, exportStartTimes[i + 1] - 1); + for (int i = 1; i < exportStartTimes.Count - 1; i++) + { + Assert.InRange(exportStartTimes[i], exportEndTimes[i - 1] + 1, exportStartTimes[i + 1] - 1); + } } } @@ -166,45 +172,48 @@ public void ExporterIsSlowerThanDelay() public void AddSpanAfterQueueIsExhausted() { int exportCalledCount = 0; - spanExporter = new TestExporter(_ => Interlocked.Increment(ref exportCalledCount)); - spanProcessor = new BatchingSpanProcessor(spanExporter, 1, TimeSpan.FromMilliseconds(100), 1); - - var spans = new List<Span>(); - for (int i = 0; i < 20; i ++) + var spanExporter = new TestExporter(_ => Interlocked.Increment(ref exportCalledCount)); + using (var spanProcessor = new BatchingSpanProcessor(spanExporter, 1, TimeSpan.FromMilliseconds(100), 1)) { - spans.Add(CreateSampledEndedSpan(i.ToString())); - } + var spans = new List<Span>(); + for (int i = 0; i < 20; i++) + { + spans.Add(CreateSampledEndedSpan(i.ToString(), spanProcessor)); + } - var exported = WaitForSpans(spanExporter, 1, DefaultTimeout); + var exported = WaitForSpans(spanExporter, 1, DefaultTimeout); - Assert.Equal(1, exportCalledCount); - Assert.InRange(exported.Length, 1,2); - Assert.Contains(spans.First(), exported); + Assert.Equal(1, exportCalledCount); + Assert.InRange(exported.Length, 1, 2); + Assert.Contains(spans.First(), exported); + } } [Fact] public void ExportMoreSpansThanTheMaxBatchSize() { int exportCalledCount = 0; - spanExporter = new TestExporter(_ => Interlocked.Increment(ref exportCalledCount)); - spanProcessor = new BatchingSpanProcessor(spanExporter, 128, DefaultDelay, 3); - var span1 = CreateSampledEndedSpan(SpanName1); - var span2 = CreateSampledEndedSpan(SpanName1); - var span3 = CreateSampledEndedSpan(SpanName1); - var span4 = CreateSampledEndedSpan(SpanName1); - var span5 = CreateSampledEndedSpan(SpanName1); - var span6 = CreateSampledEndedSpan(SpanName1); - - var exported = WaitForSpans(spanExporter, 6, DefaultTimeout); - Assert.Equal(2, exportCalledCount); - - Assert.Equal(6, exported.Count()); - Assert.Contains(span1, exported); - Assert.Contains(span2, exported); - Assert.Contains(span3, exported); - Assert.Contains(span4, exported); - Assert.Contains(span5, exported); - Assert.Contains(span6, exported); + var spanExporter = new TestExporter(_ => Interlocked.Increment(ref exportCalledCount)); + using (var spanProcessor = new BatchingSpanProcessor(spanExporter, 128, DefaultDelay, 3)) + { + var span1 = CreateSampledEndedSpan(SpanName1, spanProcessor); + var span2 = CreateSampledEndedSpan(SpanName1, spanProcessor); + var span3 = CreateSampledEndedSpan(SpanName1, spanProcessor); + var span4 = CreateSampledEndedSpan(SpanName1, spanProcessor); + var span5 = CreateSampledEndedSpan(SpanName1, spanProcessor); + var span6 = CreateSampledEndedSpan(SpanName1, spanProcessor); + + var exported = WaitForSpans(spanExporter, 6, DefaultTimeout); + Assert.InRange(exportCalledCount, 2, 6); + + Assert.Equal(6, exported.Count()); + Assert.Contains(span1, exported); + Assert.Contains(span2, exported); + Assert.Contains(span3, exported); + Assert.Contains(span4, exported); + Assert.Contains(span5, exported); + Assert.Contains(span6, exported); + } } @@ -212,56 +221,58 @@ public void ExportMoreSpansThanTheMaxBatchSize() public void ExportNotSampledSpans() { int exportCalledCount = 0; - spanExporter = new TestExporter(_ => Interlocked.Increment(ref exportCalledCount)); - spanProcessor = new BatchingSpanProcessor(spanExporter, 128, DefaultDelay, 3); - - var span1 = CreateNotSampledEndedSpan(SpanName1); - var span2 = CreateSampledEndedSpan(SpanName2); - // Spans are recorded and exported in the same order as they are ended, we test that a non - // sampled span is not exported by creating and ending a sampled span after a non sampled span - // and checking that the first exported span is the sampled span (the non sampled did not get - // exported). - var exported = WaitForSpans(spanExporter, 1, DefaultTimeout); - Assert.Equal(1, exportCalledCount); - - // Need to check this because otherwise the variable span1 is unused, other option is to not - // have a span1 variable. - Assert.Single(exported); - Assert.Contains(span2, exported); + var spanExporter = new TestExporter(_ => Interlocked.Increment(ref exportCalledCount)); + using (var spanProcessor = new BatchingSpanProcessor(spanExporter, 128, DefaultDelay, 3)) + { + var span1 = CreateNotSampledEndedSpan(SpanName1, spanProcessor); + var span2 = CreateSampledEndedSpan(SpanName2, spanProcessor); + // Spans are recorded and exported in the same order as they are ended, we test that a non + // sampled span is not exported by creating and ending a sampled span after a non sampled span + // and checking that the first exported span is the sampled span (the non sampled did not get + // exported). + var exported = WaitForSpans(spanExporter, 1, DefaultTimeout); + Assert.Equal(1, exportCalledCount); + + // Need to check this because otherwise the variable span1 is unused, other option is to not + // have a span1 variable. + Assert.Single(exported); + Assert.Contains(span2, exported); + } } [Fact] public void ProcessorDoesNotBlockOnExporter() { - spanExporter = new TestExporter( _ => Thread.Sleep(500)); - - spanProcessor = new BatchingSpanProcessor(spanExporter); - - var sampledActivity = new Activity("foo"); - sampledActivity.ActivityTraceFlags |= ActivityTraceFlags.Recorded; - sampledActivity.SetIdFormat(ActivityIdFormat.W3C); - sampledActivity.Start(); - var span = - new Span( - sampledActivity, - Enumerable.Empty<KeyValuePair<string, string>>(), - SpanKind.Internal, - new TracerConfiguration(), - spanProcessor, - PreciseTimestamp.GetUtcNow(), - default, - Resource.Empty); - - // does not block - var sw = Stopwatch.StartNew(); - span.End(); - sw.Stop(); - - Assert.InRange(sw.Elapsed, TimeSpan.Zero, TimeSpan.FromMilliseconds(100)); - - var exported = WaitForSpans(spanExporter, 1, DefaultTimeout); - - Assert.Single(exported); + var spanExporter = new TestExporter( _ => Thread.Sleep(500)); + using (var spanProcessor = new BatchingSpanProcessor(spanExporter, 128, DefaultDelay, 128)) + { + var sampledActivity = new Activity("foo"); + sampledActivity.ActivityTraceFlags |= ActivityTraceFlags.Recorded; + sampledActivity.SetIdFormat(ActivityIdFormat.W3C); + sampledActivity.Start(); + var span = + new Span( + sampledActivity, + Enumerable.Empty<KeyValuePair<string, string>>(), + SpanKind.Internal, + new TracerConfiguration(), + spanProcessor, + PreciseTimestamp.GetUtcNow(), + default, + Resource.Empty); + + // does not block + var sw = Stopwatch.StartNew(); + span.End(); + Debug.WriteLine($"[{PreciseTimestamp.GetUtcNow():o}] span.end"); + sw.Stop(); + + Assert.InRange(sw.Elapsed, TimeSpan.Zero, TimeSpan.FromMilliseconds(100)); + + var exported = WaitForSpans(spanExporter, 1, DefaultTimeout); + + Assert.Single(exported); + } } [Fact] @@ -269,23 +280,25 @@ public async Task ShutdownOnNotEmptyQueueFullFlush() { const int batchSize = 2; int exportCalledCount = 0; - spanExporter = new TestExporter(_ => Interlocked.Increment(ref exportCalledCount)); - spanProcessor = new BatchingSpanProcessor(spanExporter, 128, TimeSpan.FromMilliseconds(100), batchSize); - - var spans = new List<Span>(); - for (int i = 0; i < 100; i++) + var spanExporter = new TestExporter(_ => Interlocked.Increment(ref exportCalledCount)); + using (var spanProcessor = + new BatchingSpanProcessor(spanExporter, 128, TimeSpan.FromMilliseconds(100), batchSize)) { - spans.Add(CreateSampledEndedSpan(i.ToString())); - } + var spans = new List<Span>(); + for (int i = 0; i < 100; i++) + { + spans.Add(CreateSampledEndedSpan(i.ToString(), spanProcessor)); + } - Assert.True(spanExporter.ExportedSpans.Length < spans.Count); - using (var cts = new CancellationTokenSource(DefaultTimeout)) - { - await spanProcessor.ShutdownAsync(cts.Token); + Assert.True(spanExporter.ExportedSpans.Length < spans.Count); + using (var cts = new CancellationTokenSource(DefaultTimeout)) + { + await spanProcessor.ShutdownAsync(cts.Token); + } + + Assert.Equal(spans.Count, spanExporter.ExportedSpans.Length); + Assert.InRange(exportCalledCount, spans.Count / batchSize, spans.Count); } - - Assert.Equal(spans.Count, spanExporter.ExportedSpans.Length); - Assert.InRange(exportCalledCount, spans.Count / batchSize, spans.Count); } [Fact] @@ -296,31 +309,32 @@ public async Task ShutdownOnNotEmptyQueueNotFullFlush() // we'll need about 1.5 sec to export all spans // we export 100 spans in batches of 2, each export takes 30ms, in one thread - spanExporter = new TestExporter(_ => + var spanExporter = new TestExporter(_ => { Interlocked.Increment(ref exportCalledCount); Thread.Sleep(30); }); - spanProcessor = new BatchingSpanProcessor(spanExporter, 128, TimeSpan.FromMilliseconds(100), batchSize); - - var spans = new List<Span>(); - for (int i = 0; i < 100; i++) + using (var spanProcessor = + new BatchingSpanProcessor(spanExporter, 128, TimeSpan.FromMilliseconds(100), batchSize)) { - spans.Add(CreateSampledEndedSpan(i.ToString())); - } + var spans = new List<Span>(); + for (int i = 0; i < 100; i++) + { + spans.Add(CreateSampledEndedSpan(i.ToString(), spanProcessor)); + } - Assert.True(spanExporter.ExportedSpans.Length < spans.Count); + Assert.True(spanExporter.ExportedSpans.Length < spans.Count); - // we won't bs able to export all before cancellation will fire - using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200))) - { - await spanProcessor.ShutdownAsync(cts.Token); - } + // we won't bs able to export all before cancellation will fire + using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200))) + { + await spanProcessor.ShutdownAsync(cts.Token); + } - var exportedCount = spanExporter.ExportedSpans.Length; - Assert.True(exportedCount < spans.Count); - Assert.True(exportedCount / batchSize >= exportCalledCount); + var exportedCount = spanExporter.ExportedSpans.Length; + Assert.True(exportedCount < spans.Count); + } } [Fact] @@ -328,13 +342,13 @@ public void DisposeFlushes() { const int batchSize = 2; int exportCalledCount = 0; - spanExporter = new TestExporter(_ => Interlocked.Increment(ref exportCalledCount)); + var spanExporter = new TestExporter(_ => Interlocked.Increment(ref exportCalledCount)); var spans = new List<Span>(); - using (spanProcessor = new BatchingSpanProcessor(spanExporter, 128, TimeSpan.FromMilliseconds(100), batchSize)) + using (var spanProcessor = new BatchingSpanProcessor(spanExporter, 128, TimeSpan.FromMilliseconds(100), batchSize)) { for (int i = 0; i < 100; i++) { - spans.Add(CreateSampledEndedSpan(i.ToString())); + spans.Add(CreateSampledEndedSpan(i.ToString(), spanProcessor)); } Assert.True(spanExporter.ExportedSpans.Length < spans.Count); } @@ -345,14 +359,6 @@ public void DisposeFlushes() public void Dispose() { - using (var cts = new CancellationTokenSource()) - { - var t = spanProcessor.ShutdownAsync(cts.Token); - cts.Cancel(true); - - t.ContinueWith(_ => { }).GetAwaiter().GetResult(); - } - Activity.Current = null; } @@ -366,9 +372,9 @@ private Span[] WaitForSpans(TestExporter exporter, int spanCount, TimeSpan timeo return true; } - Thread.Sleep(0); + Thread.Sleep(10); return false; - }, timeout + TimeSpan.FromMilliseconds(20)), + }, timeout), $"Expected at least {spanCount}, got {exporter.ExportedSpans.Length}"); return exporter.ExportedSpans;