diff --git a/src/OpenTelemetry/BatchExportProcessor.cs b/src/OpenTelemetry/BatchExportProcessor.cs index 7530ea12701..1d26d2636e8 100644 --- a/src/OpenTelemetry/BatchExportProcessor.cs +++ b/src/OpenTelemetry/BatchExportProcessor.cs @@ -24,12 +24,7 @@ public abstract class BatchExportProcessor : BaseExportProcessor internal readonly int ExporterTimeoutMilliseconds; private readonly CircularBuffer circularBuffer; - private readonly Thread exporterThread; - private readonly AutoResetEvent exportTrigger = new(false); - private readonly ManualResetEvent dataExportedNotification = new(false); - private readonly ManualResetEvent shutdownTrigger = new(false); - private long shutdownDrainTarget = long.MaxValue; - private long droppedCount; + private readonly BatchExportWorker worker; private bool disposed; /// @@ -57,20 +52,15 @@ protected BatchExportProcessor( this.ScheduledDelayMilliseconds = scheduledDelayMilliseconds; this.ExporterTimeoutMilliseconds = exporterTimeoutMilliseconds; this.MaxExportBatchSize = maxExportBatchSize; - this.exporterThread = new Thread(this.ExporterProc) - { - IsBackground = true, -#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 - Name = $"OpenTelemetry-{nameof(BatchExportProcessor<>)}-{exporter.GetType().Name}", -#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 - }; - this.exporterThread.Start(); + + this.worker = this.CreateWorker(); + this.worker.Start(); } /// /// Gets the number of telemetry objects dropped by the processor. /// - internal long DroppedCount => Volatile.Read(ref this.droppedCount); + internal long DroppedCount => this.worker.DroppedCount; /// /// Gets the number of telemetry objects received by the processor. @@ -89,20 +79,14 @@ internal bool TryExport(T data) { if (this.circularBuffer.Count >= this.MaxExportBatchSize) { - try - { - this.exportTrigger.Set(); - } - catch (ObjectDisposedException) - { - } + this.worker.TriggerExport(); } return true; // enqueue succeeded } // either the queue is full or exceeded the spin limit, drop the item on the floor - Interlocked.Increment(ref this.droppedCount); + this.worker.IncrementDroppedCount(); return false; } @@ -114,113 +98,44 @@ protected override void OnExport(T data) /// protected override bool OnForceFlush(int timeoutMilliseconds) { - var tail = this.circularBuffer.RemovedCount; - var head = this.circularBuffer.AddedCount; - - if (head == tail) - { - return true; // nothing to flush - } - - try - { - this.exportTrigger.Set(); - } - catch (ObjectDisposedException) - { - return false; - } - - if (timeoutMilliseconds == 0) - { - return false; - } - - var triggers = new WaitHandle[] { this.dataExportedNotification, this.shutdownTrigger }; - - var sw = timeoutMilliseconds == Timeout.Infinite - ? null - : Stopwatch.StartNew(); - - // There is a chance that the export thread finished processing all the data from the queue, - // and signaled before we enter wait here, use polling to prevent being blocked indefinitely. - const int pollingMilliseconds = 1000; - - while (true) - { - if (sw == null) - { - try - { - WaitHandle.WaitAny(triggers, pollingMilliseconds); - } - catch (ObjectDisposedException) - { - return false; - } - } - else - { - var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - - if (timeout <= 0) - { - return this.circularBuffer.RemovedCount >= head; - } - - try - { - WaitHandle.WaitAny(triggers, Math.Min((int)timeout, pollingMilliseconds)); - } - catch (ObjectDisposedException) - { - return false; - } - } - - if (this.circularBuffer.RemovedCount >= head) - { - return true; - } - - if (Volatile.Read(ref this.shutdownDrainTarget) != long.MaxValue) - { - return false; - } - } + return this.worker.WaitForExport(timeoutMilliseconds); } /// protected override bool OnShutdown(int timeoutMilliseconds) { - Volatile.Write(ref this.shutdownDrainTarget, this.circularBuffer.AddedCount); - - try + Stopwatch? shutdownStopwatch = null; + if (timeoutMilliseconds > 0) { - this.shutdownTrigger.Set(); - } - catch (ObjectDisposedException) - { - return false; + shutdownStopwatch = Stopwatch.StartNew(); } + var result = this.worker.Shutdown(timeoutMilliseconds); + OpenTelemetrySdkEventSource.Log.DroppedExportProcessorItems(this.GetType().Name, this.exporter.GetType().Name, this.DroppedCount); if (timeoutMilliseconds == Timeout.Infinite) { - this.exporterThread.Join(); - return this.exporter.Shutdown(); + return this.exporter.Shutdown() && result; } if (timeoutMilliseconds == 0) { - return this.exporter.Shutdown(0); + return this.exporter.Shutdown(0) && result; } - var sw = Stopwatch.StartNew(); - this.exporterThread.Join(timeoutMilliseconds); - var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - return this.exporter.Shutdown((int)Math.Max(timeout, 0)); + int remainingTimeout = timeoutMilliseconds; + if (shutdownStopwatch != null) + { + shutdownStopwatch.Stop(); + remainingTimeout -= (int)shutdownStopwatch.ElapsedMilliseconds; + if (remainingTimeout < 0) + { + remainingTimeout = 0; + } + } + + return this.exporter.Shutdown(remainingTimeout) && result; } /// @@ -230,9 +145,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - this.exportTrigger.Dispose(); - this.dataExportedNotification.Dispose(); - this.shutdownTrigger.Dispose(); + this.worker?.Dispose(); } this.disposed = true; @@ -241,49 +154,27 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - private void ExporterProc() + private BatchExportWorker CreateWorker() { - var triggers = new WaitHandle[] { this.exportTrigger, this.shutdownTrigger }; - - while (true) +#if NET + // Use task-based worker for browser platform where threading may be limited + if (ThreadingHelper.IsThreadingDisabled()) { - // only wait when the queue doesn't have enough items, otherwise keep busy and send data continuously - if (this.circularBuffer.Count < this.MaxExportBatchSize) - { - try - { - WaitHandle.WaitAny(triggers, this.ScheduledDelayMilliseconds); - } - catch (ObjectDisposedException) - { - // the exporter is somehow disposed before the worker thread could finish its job - return; - } - } - - if (this.circularBuffer.Count > 0) - { - using (var batch = new Batch(this.circularBuffer, this.MaxExportBatchSize)) - { - this.exporter.Export(batch); - } - - try - { - this.dataExportedNotification.Set(); - this.dataExportedNotification.Reset(); - } - catch (ObjectDisposedException) - { - // the exporter is somehow disposed before the worker thread could finish its job - return; - } - } - - if (this.circularBuffer.RemovedCount >= Volatile.Read(ref this.shutdownDrainTarget)) - { - return; - } + return new BatchExportTaskWorker( + this.circularBuffer, + this.exporter, + this.MaxExportBatchSize, + this.ScheduledDelayMilliseconds, + this.ExporterTimeoutMilliseconds); } +#endif + + // Use thread-based worker for all other platforms + return new BatchExportThreadWorker( + this.circularBuffer, + this.exporter, + this.MaxExportBatchSize, + this.ScheduledDelayMilliseconds, + this.ExporterTimeoutMilliseconds); } } diff --git a/src/OpenTelemetry/Internal/BatchExportTaskWorker.cs b/src/OpenTelemetry/Internal/BatchExportTaskWorker.cs new file mode 100644 index 00000000000..abcd7421dda --- /dev/null +++ b/src/OpenTelemetry/Internal/BatchExportTaskWorker.cs @@ -0,0 +1,275 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; + +namespace OpenTelemetry.Internal; + +/// +/// Task-based implementation of batch export worker for environments where threading may be limited. +/// +/// The type of telemetry object to be exported. +internal sealed class BatchExportTaskWorker : BatchExportWorker + where T : class +{ + private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly SemaphoreSlim exportTrigger = new(0, 1); + private readonly TaskCompletionSource shutdownCompletionSource = new(); + private volatile TaskCompletionSource dataExportedNotification = new(); + private Task? workerTask; + private bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The circular buffer for storing telemetry objects. + /// The exporter instance. + /// The maximum batch size for exports. + /// The delay between exports in milliseconds. + /// The timeout for export operations in milliseconds. + public BatchExportTaskWorker( + CircularBuffer circularBuffer, + BaseExporter exporter, + int maxExportBatchSize, + int scheduledDelayMilliseconds, + int exporterTimeoutMilliseconds) + : base(circularBuffer, exporter, maxExportBatchSize, scheduledDelayMilliseconds, exporterTimeoutMilliseconds) + { + } + + /// + public override void Start() + { + this.workerTask = Task.Run(this.ExporterProcAsync); + } + + /// + public override bool TriggerExport() + { + if (this.cancellationTokenSource.IsCancellationRequested) + { + return false; + } + + try + { + this.exportTrigger.Release(); + return true; + } + catch (ObjectDisposedException) + { + return false; + } + catch (SemaphoreFullException) + { + // Semaphore is already signaled, export is pending + return true; + } + } + + /// + public override bool WaitForExport(int timeoutMilliseconds) + { + var tail = this.CircularBuffer.RemovedCount; + var head = this.CircularBuffer.AddedCount; + + if (head == tail) + { + return true; // nothing to flush + } + + if (!this.TriggerExport()) + { + return false; + } + + if (timeoutMilliseconds == 0) + { + return false; + } + + // On Wasm (single-threaded), calling .GetAwaiter().GetResult() would deadlock + // because there is no second thread to complete the async work. Instead, just + // trigger the export and let the async worker loop drain the buffer. Wasm apps + // don't have a real exit path so there is no risk of data loss. + if (ThreadingHelper.IsThreadingDisabled()) + { + return true; + } + + return this.WaitForExportAsync(timeoutMilliseconds, head).GetAwaiter().GetResult(); + } + + /// + public override bool Shutdown(int timeoutMilliseconds) + { + this.SetShutdownDrainTarget(this.CircularBuffer.AddedCount); + + try + { + this.cancellationTokenSource.Cancel(); + } + catch (ObjectDisposedException) + { + return false; + } + + if (this.workerTask == null) + { + return true; + } + + if (timeoutMilliseconds == Timeout.Infinite) + { + this.workerTask.Wait(); + return true; + } + + if (timeoutMilliseconds == 0) + { + return true; + } + + return this.workerTask.Wait(timeoutMilliseconds); + } + + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + this.cancellationTokenSource.Dispose(); + this.exportTrigger.Dispose(); + } + + this.disposed = true; + } + + base.Dispose(disposing); + } + + private async Task WaitForExportAsync(int timeoutMilliseconds, long targetHead) + { + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); + + // There is a chance that the export task finished processing all the data from the queue, + // and signaled before we enter wait here, use polling to prevent being blocked indefinitely. + const int pollingMilliseconds = 1000; + + while (true) + { + var timeout = pollingMilliseconds; + if (sw != null) + { + var remaining = timeoutMilliseconds - sw.ElapsedMilliseconds; + if (remaining <= 0) + { + return this.CircularBuffer.RemovedCount >= targetHead; + } + + timeout = Math.Min((int)remaining, pollingMilliseconds); + } + + try + { + using var cts = new CancellationTokenSource(timeout); + using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource( + cts.Token, + this.cancellationTokenSource.Token); + + await Task.WhenAny( + this.dataExportedNotification.Task, + this.shutdownCompletionSource.Task, + Task.Delay(timeout, combinedTokenSource.Token)).ConfigureAwait(false); +#if NET8_0_OR_GREATER + await combinedTokenSource.CancelAsync().ConfigureAwait(false); +#else + combinedTokenSource.Cancel(); +#endif + } + catch (ObjectDisposedException) + { + return false; // The worker has been disposed + } + catch (OperationCanceledException) + { + // Expected when timeout or shutdown occurs + } + + if (this.CircularBuffer.RemovedCount >= targetHead) + { + return true; + } + + if (this.ShutdownDrainTarget != long.MaxValue) + { + return false; + } + } + } + + private async Task ExporterProcAsync() + { + var cancellationToken = this.cancellationTokenSource.Token; + + try + { + while (true) + { + // only wait when the queue doesn't have enough items, otherwise keep busy and send data continuously + if (this.CircularBuffer.Count < this.MaxExportBatchSize) + { + try + { + using var delayCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + await Task.WhenAny( + this.exportTrigger.WaitAsync(cancellationToken), + Task.Delay(this.ScheduledDelayMilliseconds, delayCts.Token)).ConfigureAwait(false); +#if NET8_0_OR_GREATER + await delayCts.CancelAsync().ConfigureAwait(false); +#else + delayCts.Cancel(); +#endif + } + catch (OperationCanceledException) + { + // Continue to check if there's data to export before exiting + } + catch (ObjectDisposedException) + { + // the exporter is somehow disposed before the worker thread could finish its job + return; + } + } + + this.PerformExport(); + + // Signal that data has been exported + var previousTcs = this.dataExportedNotification; + var newTcs = new TaskCompletionSource(); + if (Interlocked.CompareExchange(ref this.dataExportedNotification, newTcs, previousTcs) == previousTcs) + { + previousTcs.TrySetResult(true); + } + + if (this.ShouldShutdown() || cancellationToken.IsCancellationRequested) + { + break; + } + } + } + catch (Exception ex) + { + // Log the exception if needed + OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.ExporterProcAsync), ex); + } + finally + { + this.shutdownCompletionSource.TrySetResult(true); + } + } +} diff --git a/src/OpenTelemetry/Internal/BatchExportThreadWorker.cs b/src/OpenTelemetry/Internal/BatchExportThreadWorker.cs new file mode 100644 index 00000000000..6731d3d4aaf --- /dev/null +++ b/src/OpenTelemetry/Internal/BatchExportThreadWorker.cs @@ -0,0 +1,223 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; + +namespace OpenTelemetry.Internal; + +/// +/// Thread-based implementation of batch export worker. +/// +/// The type of telemetry object to be exported. +internal sealed class BatchExportThreadWorker : BatchExportWorker + where T : class +{ + private readonly Thread exporterThread; + private readonly AutoResetEvent exportTrigger = new(false); + private readonly ManualResetEvent dataExportedNotification = new(false); + private readonly ManualResetEvent shutdownTrigger = new(false); + private bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The circular buffer for storing telemetry objects. + /// The exporter instance. + /// The maximum batch size for exports. + /// The delay between exports in milliseconds. + /// The timeout for export operations in milliseconds. + public BatchExportThreadWorker( + CircularBuffer circularBuffer, + BaseExporter exporter, + int maxExportBatchSize, + int scheduledDelayMilliseconds, + int exporterTimeoutMilliseconds) + : base(circularBuffer, exporter, maxExportBatchSize, scheduledDelayMilliseconds, exporterTimeoutMilliseconds) + { + this.exporterThread = new Thread(this.ExporterProc) + { + IsBackground = true, +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 + Name = $"OpenTelemetry-{nameof(BatchExportProcessor)}-{exporter.GetType().Name}", +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 + }; + } + + /// + public override void Start() + { + this.exporterThread.Start(); + } + + /// + public override bool TriggerExport() + { + try + { + this.exportTrigger.Set(); + return true; + } + catch (ObjectDisposedException) + { + return false; + } + } + + /// + public override bool WaitForExport(int timeoutMilliseconds) + { + var tail = this.CircularBuffer.RemovedCount; + var head = this.CircularBuffer.AddedCount; + + if (head == tail) + { + return true; // nothing to flush + } + + if (!this.TriggerExport()) + { + return false; + } + + if (timeoutMilliseconds == 0) + { + return false; + } + + var triggers = new WaitHandle[] { this.dataExportedNotification, this.shutdownTrigger }; + + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); + + // There is a chance that the export thread finished processing all the data from the queue, + // and signaled before we enter wait here, use polling to prevent being blocked indefinitely. + const int pollingMilliseconds = 1000; + + while (true) + { + if (sw == null) + { + try + { + WaitHandle.WaitAny(triggers, pollingMilliseconds); + } + catch (ObjectDisposedException) + { + return false; + } + } + else + { + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + + if (timeout <= 0) + { + return this.CircularBuffer.RemovedCount >= head; + } + + try + { + WaitHandle.WaitAny(triggers, Math.Min((int)timeout, pollingMilliseconds)); + } + catch (ObjectDisposedException) + { + return false; + } + } + + if (this.CircularBuffer.RemovedCount >= head) + { + return true; + } + + if (this.ShutdownDrainTarget != long.MaxValue) + { + return false; + } + } + } + + /// + public override bool Shutdown(int timeoutMilliseconds) + { + this.SetShutdownDrainTarget(this.CircularBuffer.AddedCount); + + try + { + this.shutdownTrigger.Set(); + } + catch (ObjectDisposedException) + { + return false; + } + + if (timeoutMilliseconds == Timeout.Infinite) + { + this.exporterThread.Join(); + return true; + } + + if (timeoutMilliseconds == 0) + { + return true; + } + + return this.exporterThread.Join(timeoutMilliseconds); + } + + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (!this.disposed) + { + this.disposed = true; + + this.exportTrigger.Dispose(); + this.dataExportedNotification.Dispose(); + this.shutdownTrigger.Dispose(); + } + } + + private void ExporterProc() + { + var triggers = new WaitHandle[] { this.exportTrigger, this.shutdownTrigger }; + + while (true) + { + // only wait when the queue doesn't have enough items, otherwise keep busy and send data continuously + if (this.CircularBuffer.Count < this.MaxExportBatchSize) + { + try + { + WaitHandle.WaitAny(triggers, this.ScheduledDelayMilliseconds); + } + catch (ObjectDisposedException) + { + // the exporter is somehow disposed before the worker thread could finish its job + return; + } + } + + this.PerformExport(); + + try + { + this.dataExportedNotification.Set(); + this.dataExportedNotification.Reset(); + } + catch (ObjectDisposedException) + { + // the exporter is somehow disposed before the worker thread could finish its job + return; + } + + if (this.ShouldShutdown()) + { + return; + } + } + } +} diff --git a/src/OpenTelemetry/Internal/BatchExportWorker.cs b/src/OpenTelemetry/Internal/BatchExportWorker.cs new file mode 100644 index 00000000000..e2b92023429 --- /dev/null +++ b/src/OpenTelemetry/Internal/BatchExportWorker.cs @@ -0,0 +1,158 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Internal; + +/// +/// Abstract base class for batch export workers that handle the threading and synchronization logic for batch export processors. +/// +/// The type of telemetry object to be exported. +internal abstract class BatchExportWorker : IDisposable + where T : class +{ + private long shutdownDrainTarget = long.MaxValue; + private long droppedCount; + + /// + /// Initializes a new instance of the class. + /// + /// The circular buffer for storing telemetry objects. + /// The exporter instance. + /// The maximum batch size for exports. + /// The delay between exports in milliseconds. + /// The timeout for export operations in milliseconds. + protected BatchExportWorker( + CircularBuffer circularBuffer, + BaseExporter exporter, + int maxExportBatchSize, + int scheduledDelayMilliseconds, + int exporterTimeoutMilliseconds) + { + this.CircularBuffer = circularBuffer; + this.Exporter = exporter; + this.MaxExportBatchSize = maxExportBatchSize; + this.ScheduledDelayMilliseconds = scheduledDelayMilliseconds; + this.ExporterTimeoutMilliseconds = exporterTimeoutMilliseconds; + } + + ~BatchExportWorker() + { + // Finalizer to ensure resources are cleaned up if Dispose is not called + this.Dispose(false); + } + + /// + /// Gets the number of telemetry objects dropped by the processor. + /// + internal long DroppedCount => Volatile.Read(ref this.droppedCount); + + /// + /// Gets the circular buffer for storing telemetry objects. + /// + protected CircularBuffer CircularBuffer { get; } + + /// + /// Gets the exporter instance. + /// + protected BaseExporter Exporter { get; } + + /// + /// Gets the maximum batch size for exports. + /// + protected int MaxExportBatchSize { get; } + + /// + /// Gets the delay between exports in milliseconds. + /// + protected int ScheduledDelayMilliseconds { get; } + + /// + /// Gets the timeout for export operations in milliseconds. + /// + protected int ExporterTimeoutMilliseconds { get; } + + /// + /// Gets the shutdown drain target. + /// + protected long ShutdownDrainTarget => Volatile.Read(ref this.shutdownDrainTarget); + + /// + /// Starts the worker. + /// + public abstract void Start(); + + /// + /// Triggers an export operation. + /// + /// if the export was triggered successfully; otherwise, . + public abstract bool TriggerExport(); + + /// + /// Waits for export to complete. + /// + /// The timeout in milliseconds. + /// True if the export completed within the timeout; otherwise, false. + public abstract bool WaitForExport(int timeoutMilliseconds); + + /// + /// Initiates shutdown and waits for completion. + /// + /// The timeout in milliseconds. + /// True if shutdown completed within the timeout; otherwise, false. + public abstract bool Shutdown(int timeoutMilliseconds); + + /// + /// Increments the dropped count. + /// + public void IncrementDroppedCount() + { + Interlocked.Increment(ref this.droppedCount); + } + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Sets the shutdown drain target. + /// + /// The target count. + protected void SetShutdownDrainTarget(long target) + { + Volatile.Write(ref this.shutdownDrainTarget, target); + } + + /// + /// Performs the export operation. + /// + protected void PerformExport() + { + if (this.CircularBuffer.Count > 0) + { + using (var batch = new Batch(this.CircularBuffer, this.MaxExportBatchSize)) + { + this.Exporter.Export(batch); + } + } + } + + /// + /// Checks if shutdown should occur. + /// + /// True if shutdown should occur; otherwise, false. + protected bool ShouldShutdown() + { + return this.CircularBuffer.RemovedCount >= this.ShutdownDrainTarget; + } + + /// + /// Releases the unmanaged resources used by this class and optionally releases the managed resources. + /// + /// True to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + } +} diff --git a/src/OpenTelemetry/Internal/PeriodicExportingMetricReaderTaskWorker.cs b/src/OpenTelemetry/Internal/PeriodicExportingMetricReaderTaskWorker.cs new file mode 100644 index 00000000000..be707fe95b7 --- /dev/null +++ b/src/OpenTelemetry/Internal/PeriodicExportingMetricReaderTaskWorker.cs @@ -0,0 +1,175 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Metrics; + +namespace OpenTelemetry.Internal; + +/// +/// Task-based implementation of periodic exporting metric reader worker for environments where threading may be limited. +/// +internal sealed class PeriodicExportingMetricReaderTaskWorker : PeriodicExportingMetricReaderWorker +{ + private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly SemaphoreSlim exportTrigger = new(0, 1); + private readonly TaskCompletionSource shutdownCompletionSource = new(); + private Task? workerTask; + private bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The metric reader instance. + /// The interval in milliseconds between two consecutive exports. + /// How long the export can run before it is cancelled. + public PeriodicExportingMetricReaderTaskWorker( + BaseExportingMetricReader metricReader, + int exportIntervalMilliseconds, + int exportTimeoutMilliseconds) + : base(metricReader, exportIntervalMilliseconds, exportTimeoutMilliseconds) + { + } + + /// + public override void Start() + { + this.workerTask = Task.Run(this.ExporterProcAsync); + } + + /// + public override bool TriggerExport() + { + if (this.cancellationTokenSource.IsCancellationRequested) + { + return false; + } + + try + { + this.exportTrigger.Release(); + return true; + } + catch (ObjectDisposedException) + { + return false; + } + catch (SemaphoreFullException) + { + // Semaphore is already signaled, export is pending + return true; + } + } + + /// + public override bool Shutdown(int timeoutMilliseconds) + { + try + { + this.cancellationTokenSource.Cancel(); + } + catch (ObjectDisposedException) + { + return false; + } + + if (this.workerTask == null) + { + return true; + } + + if (timeoutMilliseconds == Timeout.Infinite) + { + this.workerTask.Wait(); + return true; + } + + if (timeoutMilliseconds == 0) + { + return true; + } + + return this.workerTask.Wait(timeoutMilliseconds); + } + + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + this.cancellationTokenSource.Dispose(); + this.exportTrigger.Dispose(); + } + + this.disposed = true; + } + + base.Dispose(disposing); + } + + private async Task ExporterProcAsync() + { + var cancellationToken = this.cancellationTokenSource.Token; + var sw = Stopwatch.StartNew(); + + try + { + while (!cancellationToken.IsCancellationRequested) + { + var timeout = (int)(this.ExportIntervalMilliseconds - (sw.ElapsedMilliseconds % this.ExportIntervalMilliseconds)); + + var exportTriggerTask = this.exportTrigger.WaitAsync(cancellationToken); + Task? triggeredTask = null; + + try + { + using var delayCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + triggeredTask = await Task.WhenAny( + exportTriggerTask, + Task.Delay(timeout, delayCts.Token)).ConfigureAwait(false); +#if NET8_0_OR_GREATER + await delayCts.CancelAsync().ConfigureAwait(false); +#else + delayCts.Cancel(); +#endif + } + catch (OperationCanceledException) + { + // Continue to check if shutdown was requested + } + + if (cancellationToken.IsCancellationRequested) + { + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because Shutdown was triggered."); + this.MetricReader.Collect(this.ExportTimeoutMilliseconds); + break; + } + + // Check if the trigger was signaled by trying to acquire it with a timeout of 0 + var exportWasTriggered = triggeredTask == exportTriggerTask; + + if (exportWasTriggered) + { + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because Export was triggered."); + } + else + { + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because the export interval has elapsed."); + } + + this.MetricReader.Collect(this.ExportTimeoutMilliseconds); + } + } + catch (Exception ex) + { + // Log the exception if needed + OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.ExporterProcAsync), ex); + } + finally + { + this.shutdownCompletionSource.TrySetResult(true); + } + } +} diff --git a/src/OpenTelemetry/Internal/PeriodicExportingMetricReaderThreadWorker.cs b/src/OpenTelemetry/Internal/PeriodicExportingMetricReaderThreadWorker.cs new file mode 100644 index 00000000000..5dffebcd2d9 --- /dev/null +++ b/src/OpenTelemetry/Internal/PeriodicExportingMetricReaderThreadWorker.cs @@ -0,0 +1,140 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Metrics; + +namespace OpenTelemetry.Internal; + +/// +/// Thread-based implementation of periodic exporting metric reader worker. +/// +internal sealed class PeriodicExportingMetricReaderThreadWorker : PeriodicExportingMetricReaderWorker +{ + private readonly Thread exporterThread; + private readonly AutoResetEvent exportTrigger = new(false); + private readonly ManualResetEvent shutdownTrigger = new(false); + private bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The metric reader instance. + /// The interval in milliseconds between two consecutive exports. + /// How long the export can run before it is cancelled. + public PeriodicExportingMetricReaderThreadWorker( + BaseExportingMetricReader metricReader, + int exportIntervalMilliseconds, + int exportTimeoutMilliseconds) + : base(metricReader, exportIntervalMilliseconds, exportTimeoutMilliseconds) + { + this.exporterThread = new Thread(this.ExporterProc) + { + IsBackground = true, +#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 + Name = $"OpenTelemetry-{nameof(PeriodicExportingMetricReader)}-{metricReader.GetType().Name}", +#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 + }; + } + + /// + public override void Start() + { + this.exporterThread.Start(); + } + + /// + public override bool TriggerExport() + { + try + { + this.exportTrigger.Set(); + return true; + } + catch (ObjectDisposedException) + { + return false; + } + } + + /// + public override bool Shutdown(int timeoutMilliseconds) + { + try + { + this.shutdownTrigger.Set(); + } + catch (ObjectDisposedException) + { + return false; + } + + if (timeoutMilliseconds == Timeout.Infinite) + { + this.exporterThread.Join(); + return true; + } + + if (timeoutMilliseconds == 0) + { + return true; + } + + return this.exporterThread.Join(timeoutMilliseconds); + } + + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + this.exportTrigger.Dispose(); + this.shutdownTrigger.Dispose(); + } + + this.disposed = true; + } + + base.Dispose(disposing); + } + + private void ExporterProc() + { + int index; + int timeout; + var triggers = new WaitHandle[] { this.exportTrigger, this.shutdownTrigger }; + var sw = Stopwatch.StartNew(); + + while (true) + { + timeout = (int)(this.ExportIntervalMilliseconds - (sw.ElapsedMilliseconds % this.ExportIntervalMilliseconds)); + + try + { + index = WaitHandle.WaitAny(triggers, timeout); + } + catch (ObjectDisposedException) + { + return; + } + + switch (index) + { + case 0: // export + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because Export was triggered."); + this.MetricReader.Collect(this.ExportTimeoutMilliseconds); + break; + case 1: // shutdown + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because Shutdown was triggered."); + this.MetricReader.Collect(this.ExportTimeoutMilliseconds); + return; + case WaitHandle.WaitTimeout: // timer + OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because the export interval has elapsed."); + this.MetricReader.Collect(this.ExportTimeoutMilliseconds); + break; + } + } + } +} diff --git a/src/OpenTelemetry/Internal/PeriodicExportingMetricReaderWorker.cs b/src/OpenTelemetry/Internal/PeriodicExportingMetricReaderWorker.cs new file mode 100644 index 00000000000..7af5538e5c1 --- /dev/null +++ b/src/OpenTelemetry/Internal/PeriodicExportingMetricReaderWorker.cs @@ -0,0 +1,84 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpenTelemetry.Metrics; + +namespace OpenTelemetry.Internal; + +/// +/// Abstract base class for periodic exporting metric reader workers that handle the threading and synchronization logic. +/// +internal abstract class PeriodicExportingMetricReaderWorker : IDisposable +{ + /// + /// Initializes a new instance of the class. + /// + /// The metric reader instance. + /// The interval in milliseconds between two consecutive exports. + /// How long the export can run before it is cancelled. + protected PeriodicExportingMetricReaderWorker( + BaseExportingMetricReader metricReader, + int exportIntervalMilliseconds, + int exportTimeoutMilliseconds) + { + this.MetricReader = metricReader; + this.ExportIntervalMilliseconds = exportIntervalMilliseconds; + this.ExportTimeoutMilliseconds = exportTimeoutMilliseconds; + } + + ~PeriodicExportingMetricReaderWorker() + { + // Finalizer to ensure resources are cleaned up if Dispose is not called + this.Dispose(false); + } + + /// + /// Gets the metric reader instance. + /// + protected BaseExportingMetricReader MetricReader { get; } + + /// + /// Gets he interval in milliseconds between two consecutive exports. + /// + protected int ExportIntervalMilliseconds { get; } + + /// + /// Gets how long the export can run before it is cancelled. + /// + protected int ExportTimeoutMilliseconds { get; } + + /// + /// Starts the worker. + /// + public abstract void Start(); + + /// + /// Triggers an export operation. + /// + /// if the shutdown completed within the timeout; otherwise, . + public abstract bool TriggerExport(); + + /// + /// Initiates shutdown and waits for completion. + /// + /// The timeout in milliseconds. + /// True if the shutdown completed within the timeout; otherwise, false. + public abstract bool Shutdown(int timeoutMilliseconds); + + /// + /// Disposes of the worker and its resources. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the unmanaged resources used by this class and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + } +} diff --git a/src/OpenTelemetry/Internal/ThreadingHelper.cs b/src/OpenTelemetry/Internal/ThreadingHelper.cs new file mode 100644 index 00000000000..fbab3b659df --- /dev/null +++ b/src/OpenTelemetry/Internal/ThreadingHelper.cs @@ -0,0 +1,66 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Internal; + +/// +/// Provides helpers for determining whether threads are available and for +/// temporarily overriding that detection. The override is primarily used in +/// tests to ensure both threading modes stay covered. +/// +internal static class ThreadingHelper +{ + private static readonly AsyncLocal ThreadingDisabledOverride = new(); + + /// + /// Sets a scoped override indicating whether threading should be treated as + /// disabled. Returns an that restores the + /// previous value when disposed. + /// + /// True to treat threading as disabled for the scope. + /// A disposable scope that restores the prior threading override. + internal static IDisposable BeginThreadingOverride(bool isThreadingDisabled) + { + var scope = new ThreadingOverrideScope(ThreadingDisabledOverride.Value); + ThreadingDisabledOverride.Value = isThreadingDisabled; + return scope; + } + + /// + /// Determines whether threading should be considered unavailable for the + /// current context, honoring any scoped overrides. + /// + /// True when threading is treated as disabled for the current context. + internal static bool IsThreadingDisabled() + { + if (ThreadingDisabledOverride.Value.HasValue) + { + return ThreadingDisabledOverride.Value.Value; + } + + // if the threadpool isn't using threads assume they aren't enabled + ThreadPool.GetMaxThreads(out int workerThreads, out int completionPortThreads); + + return workerThreads == 1 && completionPortThreads == 1; + } + + private sealed class ThreadingOverrideScope : IDisposable + { + private readonly bool? previous; + private bool disposed; + + public ThreadingOverrideScope(bool? previous) + { + this.previous = previous; + } + + public void Dispose() + { + if (!this.disposed) + { + ThreadingDisabledOverride.Value = this.previous; + this.disposed = true; + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/Reader/PeriodicExportingMetricReader.cs b/src/OpenTelemetry/Metrics/Reader/PeriodicExportingMetricReader.cs index 5951d63b204..48e6c4eabb6 100644 --- a/src/OpenTelemetry/Metrics/Reader/PeriodicExportingMetricReader.cs +++ b/src/OpenTelemetry/Metrics/Reader/PeriodicExportingMetricReader.cs @@ -18,9 +18,7 @@ public class PeriodicExportingMetricReader : BaseExportingMetricReader internal readonly int ExportIntervalMilliseconds; internal readonly int ExportTimeoutMilliseconds; - private readonly Thread exporterThread; - private readonly AutoResetEvent exportTrigger = new(false); - private readonly ManualResetEvent shutdownTrigger = new(false); + private readonly PeriodicExportingMetricReaderWorker worker; private bool disposed; /// @@ -47,44 +45,43 @@ public PeriodicExportingMetricReader( this.ExportIntervalMilliseconds = exportIntervalMilliseconds; this.ExportTimeoutMilliseconds = exportTimeoutMilliseconds; - this.exporterThread = new Thread(this.ExporterProc) - { - IsBackground = true, -#pragma warning disable CA1062 // Validate arguments of public methods - needed for netstandard2.1 - Name = $"OpenTelemetry-{nameof(PeriodicExportingMetricReader)}-{exporter.GetType().Name}", -#pragma warning restore CA1062 // Validate arguments of public methods - needed for netstandard2.1 - }; - this.exporterThread.Start(); + this.worker = this.CreateWorker(); + this.worker.Start(); } /// protected override bool OnShutdown(int timeoutMilliseconds) { - var result = true; - - try + Stopwatch? shutdownStopwatch = null; + if (timeoutMilliseconds > 0) { - this.shutdownTrigger.Set(); + shutdownStopwatch = Stopwatch.StartNew(); } - catch (ObjectDisposedException) + + var result = this.worker.Shutdown(timeoutMilliseconds); + + if (timeoutMilliseconds == Timeout.Infinite) { - return false; + return this.exporter.Shutdown() && result; } - if (timeoutMilliseconds == Timeout.Infinite) + if (timeoutMilliseconds == 0) { - this.exporterThread.Join(); - result = this.exporter.Shutdown() && result; + return this.exporter.Shutdown(0) && result; } - else + + int remainingTimeout = timeoutMilliseconds; + if (shutdownStopwatch != null) { - var sw = Stopwatch.StartNew(); - result = this.exporterThread.Join(timeoutMilliseconds) && result; - var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - result = this.exporter.Shutdown((int)Math.Max(timeout, 0)) && result; + shutdownStopwatch.Stop(); + remainingTimeout -= (int)shutdownStopwatch.ElapsedMilliseconds; + if (remainingTimeout < 0) + { + remainingTimeout = 0; + } } - return result; + return this.exporter.Shutdown(remainingTimeout) && result; } /// @@ -94,8 +91,7 @@ protected override void Dispose(bool disposing) { if (disposing) { - this.exportTrigger.Dispose(); - this.shutdownTrigger.Dispose(); + this.worker?.Dispose(); } this.disposed = true; @@ -104,44 +100,26 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - private void ExporterProc() - { - int index; - int timeout; - var triggers = new WaitHandle[] { this.exportTrigger, this.shutdownTrigger }; - var sw = Stopwatch.StartNew(); +#pragma warning disable CA1859 // Change return type of method 'CreateWorker' from 'PeriodicExportingMetricReaderWorker' to 'PeriodicExportingMetricReaderThreadWorker' for improved performance - while (true) + private PeriodicExportingMetricReaderWorker CreateWorker() +#pragma warning restore CA1859 + { +#if NET + // Use task-based worker for browser platform where threading may be limited + if (ThreadingHelper.IsThreadingDisabled()) { - timeout = (int)(this.ExportIntervalMilliseconds - (sw.ElapsedMilliseconds % this.ExportIntervalMilliseconds)); - - try - { - index = WaitHandle.WaitAny(triggers, timeout); - } - catch (ObjectDisposedException) - { - return; - } - - switch (index) - { - case 0: // export - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because Export was triggered."); - this.Collect(this.ExportTimeoutMilliseconds); - break; - case 1: // shutdown - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because Shutdown was triggered."); - this.Collect(this.ExportTimeoutMilliseconds); // TODO: do we want to use the shutdown timeout here? - return; - case WaitHandle.WaitTimeout: // timer - OpenTelemetrySdkEventSource.Log.MetricReaderEvent("PeriodicExportingMetricReader calling MetricReader.Collect because the export interval has elapsed."); - this.Collect(this.ExportTimeoutMilliseconds); - break; - - default: - break; - } + return new PeriodicExportingMetricReaderTaskWorker( + this, + this.ExportIntervalMilliseconds, + this.ExportTimeoutMilliseconds); } +#endif + + // Use thread-based worker for all other platforms + return new PeriodicExportingMetricReaderThreadWorker( + this, + this.ExportIntervalMilliseconds, + this.ExportTimeoutMilliseconds); } } diff --git a/src/OpenTelemetry/Trace/Processor/BatchActivityExportProcessor.cs b/src/OpenTelemetry/Trace/Processor/BatchActivityExportProcessor.cs index 73e7968c78f..6e116c19db7 100644 --- a/src/OpenTelemetry/Trace/Processor/BatchActivityExportProcessor.cs +++ b/src/OpenTelemetry/Trace/Processor/BatchActivityExportProcessor.cs @@ -14,11 +14,11 @@ public class BatchActivityExportProcessor : BatchExportProcessor /// /// Initializes a new instance of the class. /// - /// - /// - /// - /// - /// + /// + /// + /// + /// + /// public BatchActivityExportProcessor( BaseExporter exporter, int maxQueueSize = DefaultMaxQueueSize, diff --git a/src/Shared/PeriodicExportingMetricReaderHelper.cs b/src/Shared/PeriodicExportingMetricReaderHelper.cs index 2740c7f7d7d..5acf5bfcf0f 100644 --- a/src/Shared/PeriodicExportingMetricReaderHelper.cs +++ b/src/Shared/PeriodicExportingMetricReaderHelper.cs @@ -14,13 +14,10 @@ internal static PeriodicExportingMetricReader CreatePeriodicExportingMetricReade int defaultExportIntervalMilliseconds = DefaultExportIntervalMilliseconds, int defaultExportTimeoutMilliseconds = DefaultExportTimeoutMilliseconds) { - var exportInterval = - options.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds ?? defaultExportIntervalMilliseconds; + var exportIntervalMilliseconds = options.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds ?? defaultExportIntervalMilliseconds; + var exportTimeoutMilliseconds = options.PeriodicExportingMetricReaderOptions.ExportTimeoutMilliseconds ?? defaultExportTimeoutMilliseconds; - var exportTimeout = - options.PeriodicExportingMetricReaderOptions.ExportTimeoutMilliseconds ?? defaultExportTimeoutMilliseconds; - - var metricReader = new PeriodicExportingMetricReader(exporter, exportInterval, exportTimeout) + var metricReader = new PeriodicExportingMetricReader(exporter, exportIntervalMilliseconds, exportTimeoutMilliseconds) { TemporalityPreference = options.TemporalityPreference, DefaultHistogramAggregation = options.DefaultHistogramAggregation, diff --git a/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs b/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs index 236cbe1e727..566a4fa76b9 100644 --- a/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs +++ b/test/OpenTelemetry.Tests/Internal/PeriodicExportingMetricReaderHelperTests.cs @@ -33,6 +33,20 @@ public void CreatePeriodicExportingMetricReader_Defaults() Assert.Equal(MetricReaderTemporalityPreference.Cumulative, reader.TemporalityPreference); } + [Fact] + public void CreatePeriodicExportingMetricReader_Defaults_WithTask() + { + using var threadingOverride = ThreadingHelper.BeginThreadingOverride(isThreadingDisabled: true); + +#pragma warning disable CA2000 // Dispose objects before losing scope + var reader = CreatePeriodicExportingMetricReader(); +#pragma warning restore CA2000 // Dispose objects before losing scope + + Assert.Equal(60000, reader.ExportIntervalMilliseconds); + Assert.Equal(30000, reader.ExportTimeoutMilliseconds); + Assert.Equal(MetricReaderTemporalityPreference.Cumulative, reader.TemporalityPreference); + } + [Fact] public void CreatePeriodicExportingMetricReader_TemporalityPreference_FromOptions() { diff --git a/test/OpenTelemetry.Tests/Logs/BatchLogRecordExportProcessorTests.cs b/test/OpenTelemetry.Tests/Logs/BatchLogRecordExportProcessorTests.cs index 2537ff45b84..0a9b0894dfb 100644 --- a/test/OpenTelemetry.Tests/Logs/BatchLogRecordExportProcessorTests.cs +++ b/test/OpenTelemetry.Tests/Logs/BatchLogRecordExportProcessorTests.cs @@ -4,15 +4,20 @@ #if !NETFRAMEWORK using Microsoft.Extensions.Logging; using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; using Xunit; namespace OpenTelemetry.Logs.Tests; public sealed class BatchLogRecordExportProcessorTests { - [Fact] - public void StateValuesAndScopeBufferingTest() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void StateValuesAndScopeBufferingTest(bool threadingDisabled) { + using var threadingOverride = ThreadingHelper.BeginThreadingOverride(threadingDisabled); + var scopeProvider = new LoggerExternalScopeProvider(); List exportedItems = []; @@ -21,7 +26,10 @@ public void StateValuesAndScopeBufferingTest() #pragma warning disable CA2000 // Dispose objects before losing scope new InMemoryExporter(exportedItems), #pragma warning restore CA2000 // Dispose objects before losing scope - scheduledDelayMilliseconds: int.MaxValue); + maxQueueSize: BatchLogRecordExportProcessor.DefaultMaxQueueSize, + scheduledDelayMilliseconds: int.MaxValue, + exporterTimeoutMilliseconds: BatchLogRecordExportProcessor.DefaultExporterTimeoutMilliseconds, + maxExportBatchSize: BatchLogRecordExportProcessor.DefaultMaxExportBatchSize); using var scope = scopeProvider.Push(exportedItems); @@ -148,5 +156,45 @@ public void LogRecordAddedToBatchIfNotFromAnyPoolTest() Assert.Single(exportedItems); Assert.Same(logRecord, exportedItems[0]); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DisposeWithoutShutdown(bool threadingDisabled) + { + using var threadingOverride = ThreadingHelper.BeginThreadingOverride(threadingDisabled); + + var scopeProvider = new LoggerExternalScopeProvider(); + + List exportedItems = new(); + + var processor = new BatchLogRecordExportProcessor( +#pragma warning disable CA2000 // Dispose objects before losing scope + new InMemoryExporter(exportedItems), +#pragma warning restore CA2000 // Dispose objects before losing scope + maxQueueSize: BatchLogRecordExportProcessor.DefaultMaxQueueSize, + scheduledDelayMilliseconds: int.MaxValue, + exporterTimeoutMilliseconds: BatchLogRecordExportProcessor.DefaultExporterTimeoutMilliseconds, + maxExportBatchSize: BatchLogRecordExportProcessor.DefaultMaxExportBatchSize); + + processor.Dispose(); + + using var scope = scopeProvider.Push(exportedItems); + + var pool = LogRecordSharedPool.Current; + + var logRecord = pool.Rent(); + + var state = new LogRecordTests.DisposingState("Hello world"); + + logRecord.ILoggerData.ScopeProvider = scopeProvider; + logRecord.StateValues = state; + + processor.OnEnd(logRecord); + + state.Dispose(); + + Assert.Empty(exportedItems); + } } #endif diff --git a/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs index 184ae6d8af6..75cc45bd99e 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricPointReclaimTests.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics.Metrics; +using OpenTelemetry.Internal; using OpenTelemetry.Tests; using Xunit; @@ -10,10 +11,14 @@ namespace OpenTelemetry.Metrics.Tests; public class MetricPointReclaimTests { [Theory] - [InlineData(false)] - [InlineData(true)] - public void MeasurementsAreNotDropped(bool emitMetricWithNoDimensions) + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public void MeasurementsAreNotDropped(bool emitMetricWithNoDimensions, bool threadingDisabled) { + using var threadingOverride = ThreadingHelper.BeginThreadingOverride(threadingDisabled); + using var meter = new Meter(Utils.GetCurrentMethodName()); var counter = meter.CreateCounter("MyFruitCounter"); @@ -21,7 +26,9 @@ public void MeasurementsAreNotDropped(bool emitMetricWithNoDimensions) const int MaxNumberOfDistinctMetricPoints = 4000; // Default max MetricPoints * 2 using var exporter = new CustomExporter(assertNoDroppedMeasurements: true); - using var metricReader = new PeriodicExportingMetricReader(exporter, exportIntervalMilliseconds: 10) + using var metricReader = new PeriodicExportingMetricReader( + exporter, + exportIntervalMilliseconds: 10) { TemporalityPreference = MetricReaderTemporalityPreference.Delta, };