@@ -15,6 +15,9 @@ import java.util.concurrent.Executors
15
15
import kotlin.math.min
16
16
import kotlin.math.roundToInt
17
17
import java.util.concurrent.atomic.AtomicBoolean
18
+ import java.util.concurrent.atomic.AtomicInteger
19
+ import java.util.concurrent.atomic.AtomicReference
20
+ import kotlinx.coroutines.channels.Channel
18
21
19
22
class MetricsRequestFactory : RequestFactory () {
20
23
override fun upload (apiHost : String ): HttpURLConnection {
@@ -77,21 +80,14 @@ object Telemetry: Subscriber {
77
80
var host: String = Constants .DEFAULT_API_HOST
78
81
// 1.0 is 100%, will get set by Segment setting before start()
79
82
// Values are adjusted by the sampleRate on send
80
- @Volatile private var _sampleRate : Double = 1.0
81
- var sampleRate: Double
82
- get() = _sampleRate
83
- set(value) {
84
- synchronized(this ) {
85
- _sampleRate = value
86
- }
87
- }
88
- var flushTimer: Int = 30 * 1000 // 30s
83
+ private var sampleRate = AtomicReference <Double >(1.0 )
84
+ private var flushTimer: Int = 30 * 1000 // 30s
89
85
var httpClient: HTTPClient = HTTPClient (" " , MetricsRequestFactory ())
90
86
var sendWriteKeyOnError: Boolean = true
91
87
var sendErrorLogData: Boolean = false
92
88
var errorHandler: ((Throwable ) -> Unit )? = ::logError
93
- var maxQueueSize: Int = 20
94
- var errorLogSizeMax: Int = 4000
89
+ private var maxQueueSize: Int = 20
90
+ private var errorLogSizeMax: Int = 4000
95
91
96
92
private const val MAX_QUEUE_BYTES = 28000
97
93
var maxQueueBytes: Int = MAX_QUEUE_BYTES
@@ -100,7 +96,7 @@ object Telemetry: Subscriber {
100
96
}
101
97
102
98
private val queue = ConcurrentLinkedQueue <RemoteMetric >()
103
- private var queueBytes = 0
99
+ private var queueBytes = AtomicInteger ( 0 )
104
100
private var started = AtomicBoolean (false )
105
101
private var rateLimitEndTime: Long = 0
106
102
private var flushFirstError = AtomicBoolean (true )
@@ -116,16 +112,27 @@ object Telemetry: Subscriber {
116
112
private var telemetryDispatcher: ExecutorCoroutineDispatcher = Executors .newSingleThreadExecutor().asCoroutineDispatcher()
117
113
private var telemetryJob: Job ? = null
118
114
115
+ private val flushChannel = Channel <Unit >(Channel .UNLIMITED )
116
+
117
+ // Start a coroutine to process flush requests
118
+ init {
119
+ telemetryScope.launch(telemetryDispatcher) {
120
+ for (event in flushChannel) {
121
+ performFlush()
122
+ }
123
+ }
124
+ }
125
+
119
126
/* *
120
127
* Starts the telemetry if it is enabled and not already started, and the sample rate is greater than 0.
121
128
* Called automatically when Telemetry.enable is set to true and when configuration data is received from Segment.
122
129
*/
123
130
fun start () {
124
- if (! enable || started.get() || sampleRate == 0.0 ) return
131
+ if (! enable || started.get() || sampleRate.get() == 0.0 ) return
125
132
started.set(true )
126
133
127
134
// Everything queued was sampled at default 100%, downsample adjustment and send will adjust values
128
- if (Math .random() > sampleRate) {
135
+ if (Math .random() > sampleRate.get() ) {
129
136
resetQueue()
130
137
}
131
138
@@ -170,10 +177,10 @@ object Telemetry: Subscriber {
170
177
val tags = mutableMapOf<String , String >()
171
178
buildTags(tags)
172
179
173
- if (! enable || sampleRate == 0.0 ) return
180
+ if (! enable || sampleRate.get() == 0.0 ) return
174
181
if (! metric.startsWith(METRICS_BASE_TAG )) return
175
182
if (tags.isEmpty()) return
176
- if (Math .random() > sampleRate) return
183
+ if (Math .random() > sampleRate.get() ) return
177
184
178
185
addRemoteMetric(metric, tags)
179
186
}
@@ -189,10 +196,10 @@ object Telemetry: Subscriber {
189
196
val tags = mutableMapOf<String , String >()
190
197
buildTags(tags)
191
198
192
- if (! enable || sampleRate == 0.0 ) return
199
+ if (! enable || sampleRate.get() == 0.0 ) return
193
200
if (! metric.startsWith(METRICS_BASE_TAG )) return
194
201
if (tags.isEmpty()) return
195
- if (Math .random() > sampleRate) return
202
+ if (Math .random() > sampleRate.get() ) return
196
203
197
204
var filteredTags = if (sendWriteKeyOnError) {
198
205
tags.toMap()
@@ -216,33 +223,41 @@ object Telemetry: Subscriber {
216
223
}
217
224
}
218
225
219
- @Synchronized
220
226
fun flush () {
227
+ if (! enable) return
228
+ flushChannel.trySend(Unit )
229
+ }
230
+
231
+ private fun performFlush () {
221
232
if (! enable || queue.isEmpty()) return
222
233
if (rateLimitEndTime > (System .currentTimeMillis() / 1000 ).toInt()) {
223
234
return
224
235
}
225
236
rateLimitEndTime = 0
226
-
237
+ flushFirstError.set( false )
227
238
try {
228
239
send()
229
240
} catch (error: Throwable ) {
230
241
errorHandler?.invoke(error)
231
- sampleRate = 0.0
242
+ sampleRate.set( 0.0 )
232
243
}
233
244
}
234
245
235
246
private fun send () {
236
- if (sampleRate == 0.0 ) return
237
- val sendQueue: MutableList <RemoteMetric >
238
- synchronized(queue) {
239
- sendQueue = queue.toMutableList()
240
- queue.clear()
241
- queueBytes = 0
242
- }
243
- sendQueue.forEach { m ->
244
- m.value = (m.value / sampleRate).roundToInt()
247
+ if (sampleRate.get() == 0.0 ) return
248
+ val sendQueue = mutableListOf<RemoteMetric >()
249
+ // Reset queue data size counter since all current queue items will be removed
250
+ queueBytes.set(0 )
251
+ var queueCount = queue.size
252
+ while (queueCount > 0 && ! queue.isEmpty()) {
253
+ -- queueCount
254
+ val m = queue.poll()
255
+ if (m != null ) {
256
+ m.value = (m.value / sampleRate.get()).roundToInt()
257
+ sendQueue.add(m)
258
+ }
245
259
}
260
+ assert (queue.size == 0 )
246
261
try {
247
262
// Json.encodeToString by default does not include default values
248
263
// We're using this to leave off the 'log' parameter if unset.
@@ -314,10 +329,12 @@ object Telemetry: Subscriber {
314
329
tags = fullTags
315
330
)
316
331
val newMetricSize = newMetric.toString().toByteArray().size
317
- synchronized(queue) {
318
- if (queueBytes + newMetricSize <= maxQueueBytes) {
319
- queue.add(newMetric)
320
- queueBytes + = newMetricSize
332
+ // Avoid synchronization issue by adding the size before checking.
333
+ if (queueBytes.addAndGet(newMetricSize) <= maxQueueBytes) {
334
+ queue.add(newMetric)
335
+ } else {
336
+ if (queueBytes.addAndGet(- newMetricSize) < 0 ) {
337
+ queueBytes.set(0 )
321
338
}
322
339
}
323
340
}
@@ -334,7 +351,7 @@ object Telemetry: Subscriber {
334
351
private suspend fun systemUpdate (system : com.segment.analytics.kotlin.core.System ) {
335
352
system.settings?.let { settings ->
336
353
settings.metrics[" sampleRate" ]?.jsonPrimitive?.double?.let {
337
- sampleRate = it
354
+ sampleRate.set(it)
338
355
// We don't want to start telemetry until two conditions are met:
339
356
// Telemetry.enable is set to true
340
357
// Settings from the server have adjusted the sampleRate
@@ -345,9 +362,7 @@ object Telemetry: Subscriber {
345
362
}
346
363
347
364
private fun resetQueue () {
348
- synchronized(queue) {
349
- queue.clear()
350
- queueBytes = 0
351
- }
365
+ queue.clear()
366
+ queueBytes.set(0 )
352
367
}
353
368
}
0 commit comments