// Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; using System.Runtime.CompilerServices; using OpenTelemetry.Internal; namespace OpenTelemetry; /// /// Implements processor that batches telemetry objects before calling exporter. /// /// The type of telemetry object to be exported. public abstract class BatchExportProcessor : BaseExportProcessor where T : class { internal const int DefaultMaxQueueSize = 2048; internal const int DefaultScheduledDelayMilliseconds = 5000; internal const int DefaultExporterTimeoutMilliseconds = 30000; internal const int DefaultMaxExportBatchSize = 512; internal readonly int MaxExportBatchSize; internal readonly int ScheduledDelayMilliseconds; internal readonly int ExporterTimeoutMilliseconds; private readonly CircularBuffer circularBuffer; private readonly BatchExportWorker worker; // Number of OnEnd calls currently in-flight (past the shutdown check). // OnShutdown waits for this to reach zero so those items finish enqueueing // before teardown, keeping the processed vs. already_shutdown counting race-free. private int activeOnEndCount; private int isShutdown; private bool disposed; /// /// Initializes a new instance of the class. /// /// Exporter instance. /// The maximum queue size. After the size is reached data are dropped. The default value is 2048. /// The delay interval in milliseconds between two consecutive exports. The default value is 5000. /// How long the export can run before it is cancelled. The default value is 30000. /// The maximum batch size of every export. It must be smaller or equal to maxQueueSize. The default value is 512. protected BatchExportProcessor( BaseExporter exporter, int maxQueueSize = DefaultMaxQueueSize, int scheduledDelayMilliseconds = DefaultScheduledDelayMilliseconds, int exporterTimeoutMilliseconds = DefaultExporterTimeoutMilliseconds, int maxExportBatchSize = DefaultMaxExportBatchSize) : base(exporter) { Guard.ThrowIfOutOfRange(maxQueueSize, min: 1); Guard.ThrowIfOutOfRange(maxExportBatchSize, min: 1, max: maxQueueSize, maxName: nameof(maxQueueSize)); Guard.ThrowIfOutOfRange(scheduledDelayMilliseconds, min: 1); Guard.ThrowIfOutOfRange(exporterTimeoutMilliseconds, min: 0); this.circularBuffer = new CircularBuffer(maxQueueSize); this.ScheduledDelayMilliseconds = scheduledDelayMilliseconds; this.ExporterTimeoutMilliseconds = exporterTimeoutMilliseconds; this.MaxExportBatchSize = maxExportBatchSize; this.worker = this.CreateWorker(); this.worker.Start(); } internal Action? ExportStarted { get; set; } /// /// Gets the number of telemetry objects dropped by the processor. /// internal long DroppedCount => this.worker.DroppedCount; /// /// Gets the number of telemetry objects received by the processor. /// internal long ReceivedCount => this.circularBuffer.AddedCount + this.DroppedCount; /// /// Gets the number of telemetry objects processed by the underlying exporter. /// internal long ProcessedCount => this.circularBuffer.RemovedCount; /// /// Gets a value indicating whether has been invoked. /// private bool IsShutdown => Volatile.Read(ref this.isShutdown) != 0; /// /// Marks the beginning of an call which may enqueue data. /// /// /// When this returns the caller MUST invoke once it is done enqueueing. When it returns the processor has already been shut down and the /// caller MUST NOT enqueue. /// /// if the caller may proceed to enqueue data. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool TryEnterOnEnd() { if (this.IsShutdown) { return false; } Interlocked.Increment(ref this.activeOnEndCount); if (this.IsShutdown) { Interlocked.Decrement(ref this.activeOnEndCount); return false; } return true; } /// /// Marks the end of an call started by . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void ExitOnEnd() => Interlocked.Decrement(ref this.activeOnEndCount); [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool TryExport(T data) { if (this.circularBuffer.TryAdd(data, maxSpinCount: 50000)) { if (this.circularBuffer.Count >= this.MaxExportBatchSize) { this.worker.TriggerExport(); } return true; // enqueue succeeded } // either the queue is full or exceeded the spin limit, drop the item on the floor this.worker.IncrementDroppedCount(); this.OnItemDropped(); return false; } /// /// Invoked when an item could not be enqueued and was dropped. /// internal virtual void OnItemDropped() { } /// protected override void OnExport(T data) => this.TryExport(data); /// protected override bool OnForceFlush(int timeoutMilliseconds) => this.worker.WaitForExport(timeoutMilliseconds); /// protected override bool OnShutdown(int timeoutMilliseconds) { // Note: BaseProcessor.Shutdown guarantees OnShutdown is invoked at most once, so // the previous value is discarded. Interlocked is used instead of Volatile.Write // for the full fence it provides, which TryEnterOnEnd relies on. _ = Interlocked.Exchange(ref this.isShutdown, 1); timeoutMilliseconds = this.WaitForActiveOnEndCalls(timeoutMilliseconds); long? timestamp = timeoutMilliseconds == Timeout.Infinite ? null : Stopwatch.GetTimestamp(); var result = this.worker.Shutdown(timeoutMilliseconds); OpenTelemetrySdkEventSource.Log.DroppedExportProcessorItems(this.GetType().Name, this.exporter.GetType().Name, this.DroppedCount); if (timeoutMilliseconds == Timeout.Infinite) { return this.exporter.Shutdown() && result; } if (timeoutMilliseconds == 0) { return this.exporter.Shutdown(0) && result; } if (timestamp is { } startedAt) { timeoutMilliseconds = Stopwatch.Remaining(timeoutMilliseconds, startedAt); } return this.exporter.Shutdown(timeoutMilliseconds) && result; } /// protected override void Dispose(bool disposing) { if (!this.disposed) { if (disposing) { this.worker?.Dispose(); } this.disposed = true; } base.Dispose(disposing); } private BatchExportWorker CreateWorker() { #if NET // Use task-based worker for browser platform where threading may be limited if (ThreadingHelper.IsThreadingDisabled()) { return new BatchExportTaskWorker( this.circularBuffer, this.exporter, this.MaxExportBatchSize, this.ScheduledDelayMilliseconds, this.ExporterTimeoutMilliseconds, this.OnExportStarted); } #endif // Use thread-based worker for all other platforms return new BatchExportThreadWorker( this.circularBuffer, this.exporter, this.MaxExportBatchSize, this.ScheduledDelayMilliseconds, this.ExporterTimeoutMilliseconds, this.OnExportStarted); } private void OnExportStarted(long count) => this.ExportStarted?.Invoke(count); /// /// Waits for in-flight calls to finish enqueueing so /// teardown is consistent, without exceeding the caller's shutdown budget. /// /// The shutdown timeout supplied by the caller. /// The remaining timeout available for the rest of the shutdown sequence. private int WaitForActiveOnEndCalls(int timeoutMilliseconds) { if (Volatile.Read(ref this.activeOnEndCount) == 0) { return timeoutMilliseconds; } SpinWait spinner = default; if (timeoutMilliseconds == Timeout.Infinite) { while (Volatile.Read(ref this.activeOnEndCount) != 0) { spinner.SpinOnce(); } return Timeout.Infinite; } var startedAt = Stopwatch.GetTimestamp(); int remainingMilliseconds; while ((remainingMilliseconds = Stopwatch.Remaining(timeoutMilliseconds, startedAt)) > 0 && Volatile.Read(ref this.activeOnEndCount) != 0) { spinner.SpinOnce(); } return remainingMilliseconds; } }