@kafitra/react-native-live-tracking 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +396 -0
- package/android/build.gradle +71 -0
- package/android/gradle.properties +7 -0
- package/android/src/main/AndroidManifest.xml +40 -0
- package/android/src/main/java/com/livetracking/LiveTrackingModuleImpl.kt +728 -0
- package/android/src/main/java/com/livetracking/LiveTrackingPackage.kt +16 -0
- package/android/src/main/java/com/livetracking/location/LocationEngine.kt +93 -0
- package/android/src/main/java/com/livetracking/network/NetworkListener.kt +127 -0
- package/android/src/main/java/com/livetracking/optimizer/ActivityRecognitionHandler.kt +248 -0
- package/android/src/main/java/com/livetracking/optimizer/MotionSleepManager.kt +130 -0
- package/android/src/main/java/com/livetracking/permissions/PermissionHandler.kt +145 -0
- package/android/src/main/java/com/livetracking/queue/QueueEngine.kt +167 -0
- package/android/src/main/java/com/livetracking/queue/QueuedLocation.kt +16 -0
- package/android/src/main/java/com/livetracking/queue/TrackingDatabase.kt +239 -0
- package/android/src/main/java/com/livetracking/receiver/BootReceiver.kt +53 -0
- package/android/src/main/java/com/livetracking/service/TrackingForegroundService.kt +145 -0
- package/android/src/main/java/com/livetracking/sync/FirebaseSyncEngine.kt +277 -0
- package/android/src/main/java/com/livetracking/sync/LocationDataPoint.kt +31 -0
- package/android/src/main/java/com/livetracking/sync/SyncEngineController.kt +220 -0
- package/android/src/main/java/com/livetracking/sync/SyncTargetConfig.kt +20 -0
- package/android/src/main/java/com/livetracking/sync/TargetHandler.kt +601 -0
- package/android/src/newarch/java/com/livetracking/LiveTrackingModule.kt +64 -0
- package/android/src/oldarch/java/com/livetracking/LiveTrackingModule.kt +70 -0
- package/android/src/test/java/com/livetracking/BackoffCalculationTest.kt +216 -0
- package/android/src/test/java/com/livetracking/BatchAccumulatorTest.kt +391 -0
- package/android/src/test/java/com/livetracking/BootReceiverTest.kt +247 -0
- package/android/src/test/java/com/livetracking/FirebaseSyncEngineTest.kt +337 -0
- package/android/src/test/java/com/livetracking/LocationEngineTest.kt +202 -0
- package/android/src/test/java/com/livetracking/MotionSleepManagerTest.kt +420 -0
- package/android/src/test/java/com/livetracking/OfflineQueueTest.kt +462 -0
- package/android/src/test/java/com/livetracking/PermissionHandlerTest.kt +200 -0
- package/android/src/test/java/com/livetracking/QueueEngineTest.kt +335 -0
- package/android/src/test/java/com/livetracking/SyncEngineControllerTest.kt +855 -0
- package/ios/ActivityRecognitionHandler.swift +196 -0
- package/ios/BackgroundModeHelper.swift +132 -0
- package/ios/FirebaseSyncEngine.swift +276 -0
- package/ios/LiveTracking-Bridging-Header.h +2 -0
- package/ios/LiveTracking.m +37 -0
- package/ios/LiveTracking.swift +773 -0
- package/ios/LocationDataPoint.swift +56 -0
- package/ios/LocationEngine.swift +160 -0
- package/ios/MotionSleepManager.swift +151 -0
- package/ios/NetworkListener.swift +105 -0
- package/ios/OfflineQueueManager.swift +503 -0
- package/ios/PermissionHandler.swift +148 -0
- package/ios/QueueEngine.swift +249 -0
- package/ios/SyncEngineController.swift +396 -0
- package/ios/SyncTargetConfig.swift +36 -0
- package/ios/TargetHandler.swift +715 -0
- package/ios/Tests/ActivityRecognitionHandlerTests.swift +259 -0
- package/ios/Tests/FirebaseSyncEngineTests.swift +303 -0
- package/ios/Tests/LocationEngineTests.swift +244 -0
- package/ios/Tests/MotionSleepManagerTests.swift +355 -0
- package/ios/Tests/NetworkListenerTests.swift +188 -0
- package/ios/Tests/OfflineQueueFlushTests.swift +375 -0
- package/ios/Tests/PermissionHandlerTests.swift +238 -0
- package/ios/Tests/QueueEngineTests.swift +346 -0
- package/ios/TrackingCleanup.swift +93 -0
- package/ios/TrackingNotificationManager.swift +187 -0
- package/lib/commonjs/EventEmitter.js +113 -0
- package/lib/commonjs/EventEmitter.js.map +1 -0
- package/lib/commonjs/LiveTracking.js +134 -0
- package/lib/commonjs/LiveTracking.js.map +1 -0
- package/lib/commonjs/NativeLiveTracking.js +21 -0
- package/lib/commonjs/NativeLiveTracking.js.map +1 -0
- package/lib/commonjs/filters/distanceTimeFilter.js +63 -0
- package/lib/commonjs/filters/distanceTimeFilter.js.map +1 -0
- package/lib/commonjs/index.js +103 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/serialization/locationSerializer.js +51 -0
- package/lib/commonjs/serialization/locationSerializer.js.map +1 -0
- package/lib/commonjs/types.js +77 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/utils/distance.js +63 -0
- package/lib/commonjs/utils/distance.js.map +1 -0
- package/lib/commonjs/utils/retry.js +80 -0
- package/lib/commonjs/utils/retry.js.map +1 -0
- package/lib/commonjs/validation.js +463 -0
- package/lib/commonjs/validation.js.map +1 -0
- package/lib/module/EventEmitter.js +105 -0
- package/lib/module/EventEmitter.js.map +1 -0
- package/lib/module/LiveTracking.js +127 -0
- package/lib/module/LiveTracking.js.map +1 -0
- package/lib/module/NativeLiveTracking.js +16 -0
- package/lib/module/NativeLiveTracking.js.map +1 -0
- package/lib/module/filters/distanceTimeFilter.js +58 -0
- package/lib/module/filters/distanceTimeFilter.js.map +1 -0
- package/lib/module/index.js +32 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/serialization/locationSerializer.js +45 -0
- package/lib/module/serialization/locationSerializer.js.map +1 -0
- package/lib/module/types.js +94 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/distance.js +56 -0
- package/lib/module/utils/distance.js.map +1 -0
- package/lib/module/utils/retry.js +72 -0
- package/lib/module/utils/retry.js.map +1 -0
- package/lib/module/validation.js +456 -0
- package/lib/module/validation.js.map +1 -0
- package/lib/typescript/EventEmitter.d.ts +65 -0
- package/lib/typescript/EventEmitter.d.ts.map +1 -0
- package/lib/typescript/LiveTracking.d.ts +23 -0
- package/lib/typescript/LiveTracking.d.ts.map +1 -0
- package/lib/typescript/NativeLiveTracking.d.ts +25 -0
- package/lib/typescript/NativeLiveTracking.d.ts.map +1 -0
- package/lib/typescript/filters/distanceTimeFilter.d.ts +44 -0
- package/lib/typescript/filters/distanceTimeFilter.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +21 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/serialization/locationSerializer.d.ts +39 -0
- package/lib/typescript/serialization/locationSerializer.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +217 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/utils/distance.d.ts +38 -0
- package/lib/typescript/utils/distance.d.ts.map +1 -0
- package/lib/typescript/utils/retry.d.ts +60 -0
- package/lib/typescript/utils/retry.d.ts.map +1 -0
- package/lib/typescript/validation.d.ts +26 -0
- package/lib/typescript/validation.d.ts.map +1 -0
- package/package.json +126 -0
- package/react-native-live-tracking.podspec +47 -0
- package/src/EventEmitter.ts +118 -0
- package/src/LiveTracking.ts +159 -0
- package/src/NativeLiveTracking.ts +29 -0
- package/src/filters/distanceTimeFilter.ts +75 -0
- package/src/index.ts +51 -0
- package/src/serialization/locationSerializer.ts +57 -0
- package/src/types.ts +252 -0
- package/src/utils/distance.ts +68 -0
- package/src/utils/retry.ts +75 -0
- package/src/validation.ts +552 -0
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
package com.livetracking
|
|
2
|
+
|
|
3
|
+
import com.livetracking.sync.*
|
|
4
|
+
import org.junit.Assert.*
|
|
5
|
+
import org.junit.Test
|
|
6
|
+
import java.util.concurrent.*
|
|
7
|
+
import java.util.concurrent.atomic.AtomicBoolean
|
|
8
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Unit tests for SyncEngineController and TargetHandler.
|
|
12
|
+
*
|
|
13
|
+
* Tests verify:
|
|
14
|
+
* - Multi-target parallel dispatch (Property 9: target isolation)
|
|
15
|
+
* - Batch accumulation invariant (Property 7)
|
|
16
|
+
* - Partial batch flush on stop (Property 8)
|
|
17
|
+
* - Target isolation on failure (Property 9)
|
|
18
|
+
* - Retry cancellation on newer data for set/update (Property 11)
|
|
19
|
+
*
|
|
20
|
+
* Validates: Requirements 3.1, 3.5, 4.1, 4.2, 4.4, 7.4, 7.7
|
|
21
|
+
*/
|
|
22
|
+
class SyncEngineControllerTest {
|
|
23
|
+
|
|
24
|
+
private fun makeLocation(
|
|
25
|
+
lat: Double = 37.7749,
|
|
26
|
+
lng: Double = -122.4194,
|
|
27
|
+
timestamp: Long = System.currentTimeMillis(),
|
|
28
|
+
accuracy: Float = 10.0f,
|
|
29
|
+
speed: Float? = null,
|
|
30
|
+
altitude: Double? = null,
|
|
31
|
+
bearing: Float? = null
|
|
32
|
+
): LocationDataPoint {
|
|
33
|
+
return LocationDataPoint(
|
|
34
|
+
latitude = lat,
|
|
35
|
+
longitude = lng,
|
|
36
|
+
timestamp = timestamp,
|
|
37
|
+
accuracy = accuracy,
|
|
38
|
+
speed = speed,
|
|
39
|
+
altitude = altitude,
|
|
40
|
+
bearing = bearing
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =========================================================================
|
|
45
|
+
// Property 7: Batch accumulation invariant
|
|
46
|
+
// For any SyncTarget with batchSize N > 1, after dispatching M location data
|
|
47
|
+
// points (where 1 ≤ M < N), zero write operations SHALL have been initiated.
|
|
48
|
+
// After dispatching exactly N points, exactly one batch write SHALL be initiated.
|
|
49
|
+
// Validates: Requirements 4.1, 4.2
|
|
50
|
+
// =========================================================================
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
fun `batch accumulation - no write until batchSize reached`() {
|
|
54
|
+
// Arrange: Create a TargetHandler with batchSize=5 and track writes
|
|
55
|
+
val writeCount = AtomicInteger(0)
|
|
56
|
+
|
|
57
|
+
val handler = createTestableTargetHandler(
|
|
58
|
+
config = SyncTargetConfig(path = "test/batch", method = "push", batchSize = 5),
|
|
59
|
+
onWrite = { writeCount.incrementAndGet() }
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
// Act: Dispatch 4 points (less than batchSize of 5)
|
|
63
|
+
repeat(4) {
|
|
64
|
+
handler.dispatch(makeLocation(timestamp = 1000L + it))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Allow executor to process
|
|
68
|
+
Thread.sleep(200)
|
|
69
|
+
|
|
70
|
+
// Assert: No writes should have occurred
|
|
71
|
+
assertEquals(
|
|
72
|
+
"No write should occur before batchSize is reached",
|
|
73
|
+
0, writeCount.get()
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
handler.shutdown()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Test
|
|
80
|
+
fun `batch accumulation - write initiated at exactly batchSize`() {
|
|
81
|
+
// Arrange: Create a TargetHandler with batchSize=3 and track writes
|
|
82
|
+
val writeCount = AtomicInteger(0)
|
|
83
|
+
|
|
84
|
+
val handler = createTestableTargetHandler(
|
|
85
|
+
config = SyncTargetConfig(path = "test/batch", method = "push", batchSize = 3),
|
|
86
|
+
onWrite = { writeCount.incrementAndGet() }
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
// Act: Dispatch exactly 3 points (equals batchSize)
|
|
90
|
+
repeat(3) {
|
|
91
|
+
handler.dispatch(makeLocation(timestamp = 1000L + it))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Allow executor to process
|
|
95
|
+
Thread.sleep(300)
|
|
96
|
+
|
|
97
|
+
// Assert: Exactly one write should have occurred
|
|
98
|
+
assertEquals(
|
|
99
|
+
"Exactly one write should occur when batchSize is reached",
|
|
100
|
+
1, writeCount.get()
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
handler.shutdown()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Test
|
|
107
|
+
fun `batch accumulation - two writes for 2x batchSize points`() {
|
|
108
|
+
// Arrange: batchSize=2, dispatch 4 points → expect 2 writes
|
|
109
|
+
val writeCount = AtomicInteger(0)
|
|
110
|
+
|
|
111
|
+
val handler = createTestableTargetHandler(
|
|
112
|
+
config = SyncTargetConfig(path = "test/batch", method = "push", batchSize = 2),
|
|
113
|
+
onWrite = { writeCount.incrementAndGet() }
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// Act: Dispatch 4 points
|
|
117
|
+
repeat(4) {
|
|
118
|
+
handler.dispatch(makeLocation(timestamp = 1000L + it))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Allow executor to process
|
|
122
|
+
Thread.sleep(300)
|
|
123
|
+
|
|
124
|
+
// Assert: Two writes should have occurred
|
|
125
|
+
assertEquals(
|
|
126
|
+
"Two writes should occur for 2x batchSize points",
|
|
127
|
+
2, writeCount.get()
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
handler.shutdown()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@Test
|
|
134
|
+
fun `batch accumulation - immediate write when batchSize is 1`() {
|
|
135
|
+
// Arrange: batchSize=1 means every point triggers a write
|
|
136
|
+
val writeCount = AtomicInteger(0)
|
|
137
|
+
|
|
138
|
+
val handler = createTestableTargetHandler(
|
|
139
|
+
config = SyncTargetConfig(path = "test/immediate", method = "set", batchSize = 1),
|
|
140
|
+
onWrite = { writeCount.incrementAndGet() }
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// Act: Dispatch 3 points
|
|
144
|
+
repeat(3) {
|
|
145
|
+
handler.dispatch(makeLocation(timestamp = 1000L + it))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Allow executor to process
|
|
149
|
+
Thread.sleep(300)
|
|
150
|
+
|
|
151
|
+
// Assert: 3 writes (one per point)
|
|
152
|
+
assertEquals(
|
|
153
|
+
"Each point should trigger a write when batchSize is 1",
|
|
154
|
+
3, writeCount.get()
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
handler.shutdown()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// =========================================================================
|
|
161
|
+
// Property 8: Partial batch flush on stop
|
|
162
|
+
// For any set of SyncTargets with partially-filled batches, when stop() is
|
|
163
|
+
// called, the Sync_Engine SHALL flush all accumulated points for every target.
|
|
164
|
+
// Validates: Requirements 4.4
|
|
165
|
+
// =========================================================================
|
|
166
|
+
|
|
167
|
+
@Test
|
|
168
|
+
fun `partial batch flush on stop - flushes accumulated points`() {
|
|
169
|
+
// Arrange: batchSize=10, dispatch 3 points (partial batch)
|
|
170
|
+
val writeCount = AtomicInteger(0)
|
|
171
|
+
val writtenPointCount = AtomicInteger(0)
|
|
172
|
+
|
|
173
|
+
val handler = createTestableTargetHandler(
|
|
174
|
+
config = SyncTargetConfig(path = "test/flush", method = "push", batchSize = 10),
|
|
175
|
+
onWriteWithPoints = { points ->
|
|
176
|
+
writeCount.incrementAndGet()
|
|
177
|
+
writtenPointCount.addAndGet(points)
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Act: Dispatch 3 points (less than batchSize of 10)
|
|
182
|
+
repeat(3) {
|
|
183
|
+
handler.dispatch(makeLocation(timestamp = 1000L + it))
|
|
184
|
+
}
|
|
185
|
+
Thread.sleep(200)
|
|
186
|
+
|
|
187
|
+
// Verify no write yet
|
|
188
|
+
assertEquals(0, writeCount.get())
|
|
189
|
+
|
|
190
|
+
// Act: Flush (simulates stop)
|
|
191
|
+
handler.flush()
|
|
192
|
+
|
|
193
|
+
// Assert: The partial batch should have been flushed
|
|
194
|
+
assertEquals(
|
|
195
|
+
"Flush should trigger a write for partial batch",
|
|
196
|
+
1, writeCount.get()
|
|
197
|
+
)
|
|
198
|
+
assertEquals(
|
|
199
|
+
"All 3 accumulated points should be written",
|
|
200
|
+
3, writtenPointCount.get()
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
handler.shutdown()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@Test
|
|
207
|
+
fun `partial batch flush on stop - SyncEngineController flushAll flushes all targets`() {
|
|
208
|
+
// Arrange: Two targets with partial batches
|
|
209
|
+
val target1Writes = AtomicInteger(0)
|
|
210
|
+
val target2Writes = AtomicInteger(0)
|
|
211
|
+
|
|
212
|
+
val handler1 = createTestableTargetHandler(
|
|
213
|
+
config = SyncTargetConfig(path = "target/one", method = "push", batchSize = 10),
|
|
214
|
+
onWrite = { target1Writes.incrementAndGet() }
|
|
215
|
+
)
|
|
216
|
+
val handler2 = createTestableTargetHandler(
|
|
217
|
+
config = SyncTargetConfig(path = "target/two", method = "push", batchSize = 10),
|
|
218
|
+
onWrite = { target2Writes.incrementAndGet() }
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
// Dispatch partial batches to both
|
|
222
|
+
repeat(3) { handler1.dispatch(makeLocation(timestamp = 1000L + it)) }
|
|
223
|
+
repeat(5) { handler2.dispatch(makeLocation(timestamp = 2000L + it)) }
|
|
224
|
+
Thread.sleep(200)
|
|
225
|
+
|
|
226
|
+
// Verify no writes yet
|
|
227
|
+
assertEquals(0, target1Writes.get())
|
|
228
|
+
assertEquals(0, target2Writes.get())
|
|
229
|
+
|
|
230
|
+
// Act: Flush both (simulates SyncEngineController.flushAll())
|
|
231
|
+
handler1.flush()
|
|
232
|
+
handler2.flush()
|
|
233
|
+
|
|
234
|
+
// Assert: Both targets should have flushed
|
|
235
|
+
assertEquals("Target 1 should flush partial batch", 1, target1Writes.get())
|
|
236
|
+
assertEquals("Target 2 should flush partial batch", 1, target2Writes.get())
|
|
237
|
+
|
|
238
|
+
handler1.shutdown()
|
|
239
|
+
handler2.shutdown()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@Test
|
|
243
|
+
fun `partial batch flush on stop - no write when batch is empty`() {
|
|
244
|
+
// Arrange: No points dispatched
|
|
245
|
+
val writeCount = AtomicInteger(0)
|
|
246
|
+
|
|
247
|
+
val handler = createTestableTargetHandler(
|
|
248
|
+
config = SyncTargetConfig(path = "test/empty", method = "push", batchSize = 10),
|
|
249
|
+
onWrite = { writeCount.incrementAndGet() }
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
// Act: Flush with empty batch
|
|
253
|
+
handler.flush()
|
|
254
|
+
|
|
255
|
+
// Assert: No write should occur
|
|
256
|
+
assertEquals(
|
|
257
|
+
"No write should occur when flushing an empty batch",
|
|
258
|
+
0, writeCount.get()
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
handler.shutdown()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// =========================================================================
|
|
265
|
+
// Property 9: Target isolation on failure
|
|
266
|
+
// If a write operation fails for one target, all other targets SHALL continue
|
|
267
|
+
// to receive and process new location data points independently.
|
|
268
|
+
// Validates: Requirements 3.5, 7.4
|
|
269
|
+
// =========================================================================
|
|
270
|
+
|
|
271
|
+
@Test
|
|
272
|
+
fun `target isolation - failure in one target does not block others`() {
|
|
273
|
+
// Arrange: Two handlers - one that always fails, one that succeeds
|
|
274
|
+
val failingWriteCount = AtomicInteger(0)
|
|
275
|
+
val successWriteCount = AtomicInteger(0)
|
|
276
|
+
|
|
277
|
+
val failingHandler = createTestableTargetHandler(
|
|
278
|
+
config = SyncTargetConfig(path = "target/failing", method = "set", batchSize = 1),
|
|
279
|
+
onWrite = {
|
|
280
|
+
failingWriteCount.incrementAndGet()
|
|
281
|
+
throw RuntimeException("Simulated Firebase failure")
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
val successHandler = createTestableTargetHandler(
|
|
286
|
+
config = SyncTargetConfig(path = "target/success", method = "set", batchSize = 1),
|
|
287
|
+
onWrite = { successWriteCount.incrementAndGet() }
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
// Act: Dispatch locations to both handlers
|
|
291
|
+
val location = makeLocation()
|
|
292
|
+
failingHandler.dispatch(location)
|
|
293
|
+
successHandler.dispatch(location)
|
|
294
|
+
|
|
295
|
+
// Allow processing
|
|
296
|
+
Thread.sleep(300)
|
|
297
|
+
|
|
298
|
+
// Assert: Success handler should have processed the write
|
|
299
|
+
assertTrue(
|
|
300
|
+
"Successful target should process writes regardless of other target failures",
|
|
301
|
+
successWriteCount.get() >= 1
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
failingHandler.shutdown()
|
|
305
|
+
successHandler.shutdown()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
@Test
|
|
309
|
+
fun `target isolation - multiple targets process independently`() {
|
|
310
|
+
// Arrange: Three targets with different batch sizes
|
|
311
|
+
val writes = Array(3) { AtomicInteger(0) }
|
|
312
|
+
|
|
313
|
+
val handlers = listOf(
|
|
314
|
+
createTestableTargetHandler(
|
|
315
|
+
config = SyncTargetConfig(path = "target/a", method = "push", batchSize = 1),
|
|
316
|
+
onWrite = { writes[0].incrementAndGet() }
|
|
317
|
+
),
|
|
318
|
+
createTestableTargetHandler(
|
|
319
|
+
config = SyncTargetConfig(path = "target/b", method = "push", batchSize = 2),
|
|
320
|
+
onWrite = { writes[1].incrementAndGet() }
|
|
321
|
+
),
|
|
322
|
+
createTestableTargetHandler(
|
|
323
|
+
config = SyncTargetConfig(path = "target/c", method = "push", batchSize = 3),
|
|
324
|
+
onWrite = { writes[2].incrementAndGet() }
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
// Act: Dispatch 3 locations to all handlers
|
|
329
|
+
repeat(3) { i ->
|
|
330
|
+
val loc = makeLocation(timestamp = 1000L + i)
|
|
331
|
+
handlers.forEach { it.dispatch(loc) }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
Thread.sleep(400)
|
|
335
|
+
|
|
336
|
+
// Assert: Each target should have written according to its own batchSize
|
|
337
|
+
// target/a (batchSize=1): 3 writes (one per point)
|
|
338
|
+
// target/b (batchSize=2): 1 write (2 points), 1 point still buffered
|
|
339
|
+
// target/c (batchSize=3): 1 write (3 points)
|
|
340
|
+
assertEquals("Target A (batchSize=1) should have 3 writes", 3, writes[0].get())
|
|
341
|
+
assertEquals("Target B (batchSize=2) should have 1 write", 1, writes[1].get())
|
|
342
|
+
assertEquals("Target C (batchSize=3) should have 1 write", 1, writes[2].get())
|
|
343
|
+
|
|
344
|
+
handlers.forEach { it.shutdown() }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
@Test
|
|
348
|
+
fun `target isolation - SyncEngineController dispatches to all targets in parallel`() {
|
|
349
|
+
// Arrange: Create a controller with multiple targets via JSON
|
|
350
|
+
val targetsJson = """[
|
|
351
|
+
{"path": "parallel/target1", "method": "set", "batchSize": 1},
|
|
352
|
+
{"path": "parallel/target2", "method": "push", "batchSize": 1},
|
|
353
|
+
{"path": "parallel/target3", "method": "update", "batchSize": 1}
|
|
354
|
+
]"""
|
|
355
|
+
|
|
356
|
+
// We can't easily mock Firebase in the controller, but we can verify
|
|
357
|
+
// that the controller creates handlers for all targets
|
|
358
|
+
val controller = SyncEngineController(
|
|
359
|
+
targetsJson = targetsJson,
|
|
360
|
+
firebaseService = "RTDB",
|
|
361
|
+
networkChecker = { true }
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
// Assert: All target paths are registered
|
|
365
|
+
val paths = controller.getTargetPaths()
|
|
366
|
+
assertEquals(3, paths.size)
|
|
367
|
+
assertTrue(paths.contains("parallel/target1"))
|
|
368
|
+
assertTrue(paths.contains("parallel/target2"))
|
|
369
|
+
assertTrue(paths.contains("parallel/target3"))
|
|
370
|
+
|
|
371
|
+
controller.shutdown()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// =========================================================================
|
|
375
|
+
// Property 11: Retry cancellation on newer data for overwrite targets
|
|
376
|
+
// For set/update targets with an active retry, when new data arrives,
|
|
377
|
+
// the in-progress retry SHALL be cancelled and only the newest data written.
|
|
378
|
+
// Validates: Requirements 7.7
|
|
379
|
+
// =========================================================================
|
|
380
|
+
|
|
381
|
+
@Test
|
|
382
|
+
fun `retry cancellation - set target cancels retry when newer data arrives`() {
|
|
383
|
+
// Arrange: A set target that fails on first write (triggering retry)
|
|
384
|
+
// then receives new data which should cancel the retry
|
|
385
|
+
val writeAttempts = mutableListOf<Long>() // Track timestamps of write attempts
|
|
386
|
+
val writeLatch = CountDownLatch(1)
|
|
387
|
+
var firstCallFailed = false
|
|
388
|
+
|
|
389
|
+
val handler = createTestableTargetHandlerWithRetry(
|
|
390
|
+
config = SyncTargetConfig(path = "test/set-cancel", method = "set", batchSize = 1),
|
|
391
|
+
onWrite = { timestamp ->
|
|
392
|
+
synchronized(writeAttempts) {
|
|
393
|
+
writeAttempts.add(timestamp)
|
|
394
|
+
}
|
|
395
|
+
if (!firstCallFailed) {
|
|
396
|
+
firstCallFailed = true
|
|
397
|
+
// Simulate transient failure to trigger retry
|
|
398
|
+
throw TransientWriteException("Network timeout")
|
|
399
|
+
}
|
|
400
|
+
// Subsequent calls succeed
|
|
401
|
+
writeLatch.countDown()
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
// Act: Dispatch first location (will fail and start retry)
|
|
406
|
+
handler.dispatch(makeLocation(timestamp = 1000L))
|
|
407
|
+
Thread.sleep(100) // Let the first dispatch process and fail
|
|
408
|
+
|
|
409
|
+
// Dispatch newer location (should cancel retry of old data)
|
|
410
|
+
handler.dispatch(makeLocation(timestamp = 2000L))
|
|
411
|
+
|
|
412
|
+
// Wait for the write to complete
|
|
413
|
+
writeLatch.await(3, TimeUnit.SECONDS)
|
|
414
|
+
Thread.sleep(200)
|
|
415
|
+
|
|
416
|
+
// Assert: The last written timestamp should be the newer one (2000L)
|
|
417
|
+
synchronized(writeAttempts) {
|
|
418
|
+
assertTrue(
|
|
419
|
+
"At least one write attempt should have been made",
|
|
420
|
+
writeAttempts.isNotEmpty()
|
|
421
|
+
)
|
|
422
|
+
// The final successful write should be for the newer data
|
|
423
|
+
assertEquals(
|
|
424
|
+
"The newest data (timestamp 2000) should be the last written",
|
|
425
|
+
2000L, writeAttempts.last()
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
handler.shutdown()
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
@Test
|
|
433
|
+
fun `retry cancellation - update target cancels retry when newer data arrives`() {
|
|
434
|
+
// Same as set target - update should also cancel retries on newer data
|
|
435
|
+
val writeAttempts = mutableListOf<Long>()
|
|
436
|
+
val writeLatch = CountDownLatch(1)
|
|
437
|
+
var firstCallFailed = false
|
|
438
|
+
|
|
439
|
+
val handler = createTestableTargetHandlerWithRetry(
|
|
440
|
+
config = SyncTargetConfig(path = "test/update-cancel", method = "update", batchSize = 1),
|
|
441
|
+
onWrite = { timestamp ->
|
|
442
|
+
synchronized(writeAttempts) {
|
|
443
|
+
writeAttempts.add(timestamp)
|
|
444
|
+
}
|
|
445
|
+
if (!firstCallFailed) {
|
|
446
|
+
firstCallFailed = true
|
|
447
|
+
throw TransientWriteException("Network timeout")
|
|
448
|
+
}
|
|
449
|
+
writeLatch.countDown()
|
|
450
|
+
}
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
// Act: Dispatch first location (will fail and start retry)
|
|
454
|
+
handler.dispatch(makeLocation(timestamp = 1000L))
|
|
455
|
+
Thread.sleep(100)
|
|
456
|
+
|
|
457
|
+
// Dispatch newer location (should cancel retry)
|
|
458
|
+
handler.dispatch(makeLocation(timestamp = 2000L))
|
|
459
|
+
|
|
460
|
+
writeLatch.await(3, TimeUnit.SECONDS)
|
|
461
|
+
Thread.sleep(200)
|
|
462
|
+
|
|
463
|
+
// Assert: Final write should be for newer data
|
|
464
|
+
synchronized(writeAttempts) {
|
|
465
|
+
assertTrue(writeAttempts.isNotEmpty())
|
|
466
|
+
assertEquals(
|
|
467
|
+
"Update target should write newest data after cancelling retry",
|
|
468
|
+
2000L, writeAttempts.last()
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
handler.shutdown()
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
@Test
|
|
476
|
+
fun `retry cancellation - push target does NOT cancel retry on newer data`() {
|
|
477
|
+
// Push targets should NOT cancel retries - all data must be preserved
|
|
478
|
+
val writeCount = AtomicInteger(0)
|
|
479
|
+
|
|
480
|
+
val handler = createTestableTargetHandler(
|
|
481
|
+
config = SyncTargetConfig(path = "test/push-no-cancel", method = "push", batchSize = 1),
|
|
482
|
+
onWrite = { writeCount.incrementAndGet() }
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
// Act: Dispatch multiple locations rapidly
|
|
486
|
+
repeat(3) {
|
|
487
|
+
handler.dispatch(makeLocation(timestamp = 1000L + it))
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
Thread.sleep(400)
|
|
491
|
+
|
|
492
|
+
// Assert: All writes should proceed (no cancellation for push)
|
|
493
|
+
assertEquals(
|
|
494
|
+
"Push target should write all data without cancellation",
|
|
495
|
+
3, writeCount.get()
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
handler.shutdown()
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// =========================================================================
|
|
502
|
+
// SyncEngineController JSON parsing and multi-target creation tests
|
|
503
|
+
// Validates: Requirements 3.1
|
|
504
|
+
// =========================================================================
|
|
505
|
+
|
|
506
|
+
@Test
|
|
507
|
+
fun `SyncEngineController parses valid targets JSON`() {
|
|
508
|
+
val targetsJson = """[
|
|
509
|
+
{"path": "users/abc/location", "method": "set"},
|
|
510
|
+
{"path": "trips/xyz/history", "method": "push", "batchSize": 20, "offlineQueue": true}
|
|
511
|
+
]"""
|
|
512
|
+
|
|
513
|
+
val controller = SyncEngineController(
|
|
514
|
+
targetsJson = targetsJson,
|
|
515
|
+
firebaseService = "RTDB",
|
|
516
|
+
networkChecker = { true }
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
val paths = controller.getTargetPaths()
|
|
520
|
+
assertEquals(2, paths.size)
|
|
521
|
+
assertEquals("users/abc/location", paths[0])
|
|
522
|
+
assertEquals("trips/xyz/history", paths[1])
|
|
523
|
+
|
|
524
|
+
controller.shutdown()
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
@Test(expected = IllegalArgumentException::class)
|
|
528
|
+
fun `SyncEngineController rejects empty targets array`() {
|
|
529
|
+
SyncEngineController(
|
|
530
|
+
targetsJson = "[]",
|
|
531
|
+
firebaseService = "RTDB",
|
|
532
|
+
networkChecker = { true }
|
|
533
|
+
)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
@Test(expected = IllegalArgumentException::class)
|
|
537
|
+
fun `SyncEngineController rejects invalid JSON`() {
|
|
538
|
+
SyncEngineController(
|
|
539
|
+
targetsJson = "not valid json",
|
|
540
|
+
firebaseService = "RTDB",
|
|
541
|
+
networkChecker = { true }
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
@Test(expected = IllegalArgumentException::class)
|
|
546
|
+
fun `SyncEngineController rejects target missing path`() {
|
|
547
|
+
SyncEngineController(
|
|
548
|
+
targetsJson = """[{"method": "set"}]""",
|
|
549
|
+
firebaseService = "RTDB",
|
|
550
|
+
networkChecker = { true }
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
@Test(expected = IllegalArgumentException::class)
|
|
555
|
+
fun `SyncEngineController rejects target missing method`() {
|
|
556
|
+
SyncEngineController(
|
|
557
|
+
targetsJson = """[{"path": "test/path"}]""",
|
|
558
|
+
firebaseService = "RTDB",
|
|
559
|
+
networkChecker = { true }
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
@Test(expected = IllegalArgumentException::class)
|
|
564
|
+
fun `SyncEngineController rejects target with invalid method`() {
|
|
565
|
+
SyncEngineController(
|
|
566
|
+
targetsJson = """[{"path": "test/path", "method": "delete"}]""",
|
|
567
|
+
firebaseService = "RTDB",
|
|
568
|
+
networkChecker = { true }
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
@Test
|
|
573
|
+
fun `SyncEngineController getQueuedCounts returns zero for non-queuing targets`() {
|
|
574
|
+
val targetsJson = """[
|
|
575
|
+
{"path": "target/a", "method": "set"},
|
|
576
|
+
{"path": "target/b", "method": "push"}
|
|
577
|
+
]"""
|
|
578
|
+
|
|
579
|
+
val controller = SyncEngineController(
|
|
580
|
+
targetsJson = targetsJson,
|
|
581
|
+
firebaseService = "RTDB",
|
|
582
|
+
networkChecker = { true }
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
val counts = controller.getQueuedCounts()
|
|
586
|
+
assertEquals(0, counts["target/a"])
|
|
587
|
+
assertEquals(0, counts["target/b"])
|
|
588
|
+
|
|
589
|
+
controller.shutdown()
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// =========================================================================
|
|
593
|
+
// TargetHandler - offline behavior tests
|
|
594
|
+
// =========================================================================
|
|
595
|
+
|
|
596
|
+
@Test
|
|
597
|
+
fun `TargetHandler discards data when offline and offlineQueue disabled`() {
|
|
598
|
+
val writeCount = AtomicInteger(0)
|
|
599
|
+
|
|
600
|
+
val handler = createTestableTargetHandler(
|
|
601
|
+
config = SyncTargetConfig(path = "test/discard", method = "set", batchSize = 1, offlineQueue = false),
|
|
602
|
+
networkOnline = false,
|
|
603
|
+
onWrite = { writeCount.incrementAndGet() }
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
// Dispatch while offline
|
|
607
|
+
handler.dispatch(makeLocation())
|
|
608
|
+
Thread.sleep(200)
|
|
609
|
+
|
|
610
|
+
// No write should occur (data discarded)
|
|
611
|
+
assertEquals(0, writeCount.get())
|
|
612
|
+
|
|
613
|
+
handler.shutdown()
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
@Test
|
|
617
|
+
fun `TargetHandler does not dispatch after shutdown`() {
|
|
618
|
+
val writeCount = AtomicInteger(0)
|
|
619
|
+
|
|
620
|
+
val handler = createTestableTargetHandler(
|
|
621
|
+
config = SyncTargetConfig(path = "test/shutdown", method = "set", batchSize = 1),
|
|
622
|
+
onWrite = { writeCount.incrementAndGet() }
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
// Shutdown first
|
|
626
|
+
handler.shutdown()
|
|
627
|
+
|
|
628
|
+
// Dispatch after shutdown
|
|
629
|
+
handler.dispatch(makeLocation())
|
|
630
|
+
Thread.sleep(200)
|
|
631
|
+
|
|
632
|
+
// No write should occur
|
|
633
|
+
assertEquals(0, writeCount.get())
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// =========================================================================
|
|
637
|
+
// Helper: Testable TargetHandler that bypasses Firebase
|
|
638
|
+
// =========================================================================
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Creates a TargetHandler subclass that intercepts write operations
|
|
642
|
+
* for testing without requiring Firebase SDK initialization.
|
|
643
|
+
*/
|
|
644
|
+
private fun createTestableTargetHandler(
|
|
645
|
+
config: SyncTargetConfig,
|
|
646
|
+
networkOnline: Boolean = true,
|
|
647
|
+
onWrite: (() -> Unit)? = null,
|
|
648
|
+
onWriteWithPoints: ((Int) -> Unit)? = null
|
|
649
|
+
): TestableTargetHandler {
|
|
650
|
+
return TestableTargetHandler(
|
|
651
|
+
config = config,
|
|
652
|
+
networkOnline = networkOnline,
|
|
653
|
+
onWrite = onWrite,
|
|
654
|
+
onWriteWithPoints = onWriteWithPoints
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private fun createTestableTargetHandlerWithRetry(
|
|
659
|
+
config: SyncTargetConfig,
|
|
660
|
+
onWrite: (Long) -> Unit
|
|
661
|
+
): TestableTargetHandlerWithRetry {
|
|
662
|
+
return TestableTargetHandlerWithRetry(
|
|
663
|
+
config = config,
|
|
664
|
+
onWrite = onWrite
|
|
665
|
+
)
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Exception class to simulate transient Firebase write failures in tests.
|
|
671
|
+
*/
|
|
672
|
+
class TransientWriteException(message: String) : RuntimeException(message)
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* A testable TargetHandler that bypasses Firebase SDK calls.
|
|
676
|
+
* Instead of writing to Firebase, it invokes the provided callbacks.
|
|
677
|
+
*
|
|
678
|
+
* This allows testing batch accumulation, flush, and dispatch logic
|
|
679
|
+
* without requiring Firebase initialization.
|
|
680
|
+
*/
|
|
681
|
+
class TestableTargetHandler(
|
|
682
|
+
private val config: SyncTargetConfig,
|
|
683
|
+
private val networkOnline: Boolean = true,
|
|
684
|
+
private val onWrite: (() -> Unit)? = null,
|
|
685
|
+
private val onWriteWithPoints: ((Int) -> Unit)? = null
|
|
686
|
+
) {
|
|
687
|
+
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
688
|
+
private val batch: MutableList<LocationDataPoint> = mutableListOf()
|
|
689
|
+
private val isShutdown = AtomicBoolean(false)
|
|
690
|
+
|
|
691
|
+
val targetPath: String get() = config.path
|
|
692
|
+
|
|
693
|
+
fun dispatch(location: LocationDataPoint) {
|
|
694
|
+
if (isShutdown.get()) return
|
|
695
|
+
|
|
696
|
+
executor.execute {
|
|
697
|
+
// Check if device is offline
|
|
698
|
+
if (!networkOnline) {
|
|
699
|
+
// Discard if offlineQueue is disabled (simplified for testing)
|
|
700
|
+
return@execute
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Accumulate in batch
|
|
704
|
+
batch.add(location)
|
|
705
|
+
|
|
706
|
+
// Check if batch is full
|
|
707
|
+
if (batch.size >= config.batchSize) {
|
|
708
|
+
flushBatch()
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
fun flush() {
|
|
714
|
+
if (isShutdown.get()) return
|
|
715
|
+
|
|
716
|
+
val future = executor.submit {
|
|
717
|
+
if (batch.isNotEmpty()) {
|
|
718
|
+
flushBatch()
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
future.get()
|
|
723
|
+
} catch (e: Exception) {
|
|
724
|
+
// Ignore
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
fun shutdown() {
|
|
729
|
+
isShutdown.set(true)
|
|
730
|
+
executor.shutdown()
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private fun flushBatch() {
|
|
734
|
+
if (batch.isEmpty()) return
|
|
735
|
+
|
|
736
|
+
val pointCount = batch.size
|
|
737
|
+
batch.clear()
|
|
738
|
+
|
|
739
|
+
try {
|
|
740
|
+
onWrite?.invoke()
|
|
741
|
+
onWriteWithPoints?.invoke(pointCount)
|
|
742
|
+
} catch (e: Exception) {
|
|
743
|
+
// Simulated failure - target isolation means we don't propagate
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* A testable TargetHandler that supports retry cancellation testing.
|
|
750
|
+
* For set/update targets, newer data cancels in-progress retries.
|
|
751
|
+
*/
|
|
752
|
+
class TestableTargetHandlerWithRetry(
|
|
753
|
+
private val config: SyncTargetConfig,
|
|
754
|
+
private val onWrite: (Long) -> Unit
|
|
755
|
+
) {
|
|
756
|
+
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
757
|
+
private val batch: MutableList<LocationDataPoint> = mutableListOf()
|
|
758
|
+
private val isShutdown = AtomicBoolean(false)
|
|
759
|
+
private val retryGeneration = AtomicInteger(0)
|
|
760
|
+
private val isRetrying = AtomicBoolean(false)
|
|
761
|
+
|
|
762
|
+
private val maxRetries: Int
|
|
763
|
+
get() = if (config.method == "push") 5 else 3
|
|
764
|
+
|
|
765
|
+
fun dispatch(location: LocationDataPoint) {
|
|
766
|
+
if (isShutdown.get()) return
|
|
767
|
+
|
|
768
|
+
executor.execute {
|
|
769
|
+
// For set/update targets, cancel any in-progress retry
|
|
770
|
+
if (config.method == "set" || config.method == "update") {
|
|
771
|
+
if (isRetrying.get()) {
|
|
772
|
+
retryGeneration.incrementAndGet()
|
|
773
|
+
isRetrying.set(false)
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Accumulate in batch
|
|
778
|
+
batch.add(location)
|
|
779
|
+
|
|
780
|
+
// Check if batch is full
|
|
781
|
+
if (batch.size >= config.batchSize) {
|
|
782
|
+
flushBatch()
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
fun shutdown() {
|
|
788
|
+
isShutdown.set(true)
|
|
789
|
+
retryGeneration.incrementAndGet()
|
|
790
|
+
executor.shutdown()
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private fun flushBatch() {
|
|
794
|
+
if (batch.isEmpty()) return
|
|
795
|
+
|
|
796
|
+
val points = ArrayList(batch)
|
|
797
|
+
batch.clear()
|
|
798
|
+
|
|
799
|
+
val generation = retryGeneration.get()
|
|
800
|
+
executeWithRetry(generation, points)
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private fun executeWithRetry(generation: Int, points: List<LocationDataPoint>) {
|
|
804
|
+
isRetrying.set(true)
|
|
805
|
+
var attempt = 0
|
|
806
|
+
|
|
807
|
+
fun tryOperation() {
|
|
808
|
+
// Check if superseded
|
|
809
|
+
if (retryGeneration.get() != generation) {
|
|
810
|
+
isRetrying.set(false)
|
|
811
|
+
return
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
attempt++
|
|
815
|
+
val lastPoint = if (config.method == "push") points.first() else points.last()
|
|
816
|
+
|
|
817
|
+
try {
|
|
818
|
+
onWrite(lastPoint.timestamp)
|
|
819
|
+
isRetrying.set(false)
|
|
820
|
+
} catch (e: TransientWriteException) {
|
|
821
|
+
// Check if superseded before retrying
|
|
822
|
+
if (retryGeneration.get() != generation) {
|
|
823
|
+
isRetrying.set(false)
|
|
824
|
+
return
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (attempt >= maxRetries) {
|
|
828
|
+
isRetrying.set(false)
|
|
829
|
+
return
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Short delay for testing (not real backoff)
|
|
833
|
+
try {
|
|
834
|
+
Thread.sleep(50)
|
|
835
|
+
} catch (ie: InterruptedException) {
|
|
836
|
+
Thread.currentThread().interrupt()
|
|
837
|
+
isRetrying.set(false)
|
|
838
|
+
return
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Check again after sleep
|
|
842
|
+
if (retryGeneration.get() != generation) {
|
|
843
|
+
isRetrying.set(false)
|
|
844
|
+
return
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
tryOperation()
|
|
848
|
+
} catch (e: Exception) {
|
|
849
|
+
isRetrying.set(false)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
tryOperation()
|
|
854
|
+
}
|
|
855
|
+
}
|