1
1
package com.amplitude.core.platform
2
2
3
3
import com.amplitude.core.Amplitude
4
+ import com.amplitude.core.Storage
4
5
import com.amplitude.core.events.BaseEvent
6
+ import com.amplitude.core.utilities.ExponentialBackoffRetryHandler
5
7
import com.amplitude.core.utilities.http.HttpClient
6
8
import com.amplitude.core.utilities.http.HttpClientInterface
9
+ import com.amplitude.core.utilities.http.ResponseHandler
7
10
import com.amplitude.core.utilities.logWithStackTrace
11
+ import kotlinx.coroutines.CoroutineScope
8
12
import kotlinx.coroutines.channels.Channel
9
13
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
10
14
import kotlinx.coroutines.channels.consumeEach
@@ -17,21 +21,24 @@ import java.util.concurrent.atomic.AtomicInteger
17
21
18
22
class EventPipeline (
19
23
private val amplitude : Amplitude ,
20
- ) {
21
- private val writeChannel: Channel <WriteQueueMessage >
22
- private val uploadChannel: Channel <String >
23
- private val eventCount: AtomicInteger = AtomicInteger (0 )
24
+ private val eventCount : AtomicInteger = AtomicInteger (0),
24
25
private val httpClient : HttpClientInterface = amplitude.configuration.httpClient
25
- ? : HttpClient (amplitude.configuration)
26
- private val storage get() = amplitude.storage
27
- private val scope get() = amplitude.amplitudeScope
26
+ ? : HttpClient (amplitude.configuration),
27
+ private val retryUploadHandler : ExponentialBackoffRetryHandler =
28
+ ExponentialBackoffRetryHandler (),
29
+ private val storage : Storage = amplitude.storage,
30
+ private val scope : CoroutineScope = amplitude.amplitudeScope,
31
+ private val writeChannel : Channel <WriteQueueMessage > = Channel (UNLIMITED ),
32
+ private var uploadChannel : Channel <String > = Channel (UNLIMITED ),
33
+ overrideResponseHandler : ResponseHandler ? = null ,
34
+ ) {
28
35
29
36
private var running: Boolean
30
37
private var scheduled: Boolean
31
38
var flushSizeDivider: AtomicInteger = AtomicInteger (1 )
32
39
33
40
private val responseHandler by lazy {
34
- storage.getResponseHandler(
41
+ overrideResponseHandler ? : storage.getResponseHandler(
35
42
this @EventPipeline,
36
43
amplitude.configuration,
37
44
scope,
@@ -40,16 +47,13 @@ class EventPipeline(
40
47
}
41
48
42
49
companion object {
43
- internal const val UPLOAD_SIG = " #!upload"
50
+ private const val UPLOAD_SIG = " #!upload"
44
51
}
45
52
46
53
init {
47
54
running = false
48
55
scheduled = false
49
56
50
- writeChannel = Channel (UNLIMITED )
51
- uploadChannel = Channel (UNLIMITED )
52
-
53
57
registerShutdownHook()
54
58
}
55
59
@@ -107,7 +111,7 @@ class EventPipeline(
107
111
108
112
private fun upload () =
109
113
scope.launch(amplitude.networkIODispatcher) {
110
- uploadChannel.consumeEach {
114
+ uploadChannel.consumeEach { signal ->
111
115
withContext(amplitude.storageIODispatcher) {
112
116
try {
113
117
storage.rollover()
@@ -118,25 +122,48 @@ class EventPipeline(
118
122
}
119
123
}
120
124
121
- val eventsData = storage.readEventsContent()
122
- for (events in eventsData) {
123
- try {
124
- val eventsString = storage.getEventsString(events)
125
- if (eventsString.isEmpty()) continue
125
+ uploadNextEventFile()
126
+ }
127
+ }
126
128
127
- val diagnostics = amplitude.diagnostics.extractDiagnostics()
128
- val response = httpClient.upload(eventsString, diagnostics)
129
- responseHandler.handle(response, events, eventsString)
130
- } catch (e: FileNotFoundException ) {
131
- e.message?.let {
132
- amplitude.logger.warn(" Event storage file not found: $it " )
133
- }
134
- } catch (e: Exception ) {
135
- e.logWithStackTrace(amplitude.logger, " Error when uploading event" )
129
+ private suspend fun uploadNextEventFile () {
130
+ try {
131
+ // only get first event file, we want to upload them one by one, in order
132
+ val eventFile = storage.readEventsContent().firstOrNull() ? : return
133
+ val eventsString = storage.getEventsString(eventFile)
134
+ if (eventsString.isEmpty()) return
135
+
136
+ val diagnostics = amplitude.diagnostics.extractDiagnostics()
137
+ val response = httpClient.upload(eventsString, diagnostics)
138
+ val handled = responseHandler.handle(response, eventFile, eventsString)
139
+
140
+ when {
141
+ handled -> retryUploadHandler.reset()
142
+ ! handled && retryUploadHandler.canRetry() -> {
143
+ retryUploadHandler.retryWithDelay {
144
+ uploadChannel.trySend(UPLOAD_SIG )
136
145
}
137
146
}
147
+ else -> {
148
+ amplitude.logger.error(
149
+ " Upload failed ${retryUploadHandler.maxRetryAttempt} times. " +
150
+ " Cancel all uploads as we might be offline."
151
+ )
152
+
153
+ // cancel all previous signals in the upload channel and reset
154
+ uploadChannel.cancel()
155
+ uploadChannel = Channel (UNLIMITED )
156
+ retryUploadHandler.reset()
157
+ }
158
+ }
159
+ } catch (e: FileNotFoundException ) {
160
+ e.message?.let {
161
+ amplitude.logger.warn(" Event storage file not found: $it " )
138
162
}
163
+ } catch (e: Exception ) {
164
+ e.logWithStackTrace(amplitude.logger, " Error when uploading event" )
139
165
}
166
+ }
140
167
141
168
private fun getFlushCount (): Int {
142
169
val count = amplitude.configuration.flushQueueSize / flushSizeDivider.get()
0 commit comments