-
Notifications
You must be signed in to change notification settings - Fork 764
/
BatchExportProcessor.cs
289 lines (248 loc) · 9.45 KB
/
BatchExportProcessor.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
using System.Runtime.CompilerServices;
using OpenTelemetry.Internal;
namespace OpenTelemetry;
/// <summary>
/// Implements processor that batches telemetry objects before calling exporter.
/// </summary>
/// <typeparam name="T">The type of telemetry object to be exported.</typeparam>
public abstract class BatchExportProcessor<T> : BaseExportProcessor<T>
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<T> 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 bool disposed;
/// <summary>
/// Initializes a new instance of the <see cref="BatchExportProcessor{T}"/> class.
/// </summary>
/// <param name="exporter">Exporter instance.</param>
/// <param name="maxQueueSize">The maximum queue size. After the size is reached data are dropped. The default value is 2048.</param>
/// <param name="scheduledDelayMilliseconds">The delay interval in milliseconds between two consecutive exports. The default value is 5000.</param>
/// <param name="exporterTimeoutMilliseconds">How long the export can run before it is cancelled. The default value is 30000.</param>
/// <param name="maxExportBatchSize">The maximum batch size of every export. It must be smaller or equal to maxQueueSize. The default value is 512.</param>
protected BatchExportProcessor(
BaseExporter<T> 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<T>(maxQueueSize);
this.ScheduledDelayMilliseconds = scheduledDelayMilliseconds;
this.ExporterTimeoutMilliseconds = exporterTimeoutMilliseconds;
this.MaxExportBatchSize = maxExportBatchSize;
this.exporterThread = new Thread(this.ExporterProc)
{
IsBackground = true,
Name = $"OpenTelemetry-{nameof(BatchExportProcessor<T>)}-{exporter.GetType().Name}",
};
this.exporterThread.Start();
}
/// <summary>
/// Gets the number of telemetry objects dropped by the processor.
/// </summary>
internal long DroppedCount => Volatile.Read(ref this.droppedCount);
/// <summary>
/// Gets the number of telemetry objects received by the processor.
/// </summary>
internal long ReceivedCount => this.circularBuffer.AddedCount + this.DroppedCount;
/// <summary>
/// Gets the number of telemetry objects processed by the underlying exporter.
/// </summary>
internal long ProcessedCount => this.circularBuffer.RemovedCount;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool TryExport(T data)
{
if (this.circularBuffer.TryAdd(data, maxSpinCount: 50000))
{
if (this.circularBuffer.Count >= this.MaxExportBatchSize)
{
try
{
this.exportTrigger.Set();
}
catch (ObjectDisposedException)
{
}
}
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);
return false;
}
/// <inheritdoc/>
protected override void OnExport(T data)
{
this.TryExport(data);
}
/// <inheritdoc/>
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;
}
}
}
/// <inheritdoc/>
protected override bool OnShutdown(int timeoutMilliseconds)
{
Volatile.Write(ref this.shutdownDrainTarget, this.circularBuffer.AddedCount);
try
{
this.shutdownTrigger.Set();
}
catch (ObjectDisposedException)
{
return false;
}
OpenTelemetrySdkEventSource.Log.DroppedExportProcessorItems(this.GetType().Name, this.exporter.GetType().Name, this.DroppedCount);
if (timeoutMilliseconds == Timeout.Infinite)
{
this.exporterThread.Join();
return this.exporter.Shutdown();
}
if (timeoutMilliseconds == 0)
{
return this.exporter.Shutdown(0);
}
var sw = Stopwatch.StartNew();
this.exporterThread.Join(timeoutMilliseconds);
var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds;
return this.exporter.Shutdown((int)Math.Max(timeout, 0));
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
this.exportTrigger.Dispose();
this.dataExportedNotification.Dispose();
this.shutdownTrigger.Dispose();
}
this.disposed = true;
}
base.Dispose(disposing);
}
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;
}
}
if (this.circularBuffer.Count > 0)
{
using (var batch = new Batch<T>(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;
}
}
}
}