@onekeyfe/react-native-range-downloader 6.7.0-alpha.311 → 6.7.0-alpha.314
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.
|
@@ -100,7 +100,10 @@ class ConcurrentRangeDownloader(
|
|
|
100
100
|
* segment files, so no in-flight worker resurrects a deleted file.
|
|
101
101
|
*/
|
|
102
102
|
class CancelHandle {
|
|
103
|
+
// Only an explicit caller cancellation sets aborted. Worker failures use
|
|
104
|
+
// workerStop to stop sibling segments without changing that intent.
|
|
103
105
|
val aborted = AtomicBoolean(false)
|
|
106
|
+
internal val workerStop = AtomicBoolean(false)
|
|
104
107
|
|
|
105
108
|
@Volatile
|
|
106
109
|
private var pool: java.util.concurrent.ExecutorService? = null
|
|
@@ -108,7 +111,7 @@ class ConcurrentRangeDownloader(
|
|
|
108
111
|
internal fun attach(pool: java.util.concurrent.ExecutorService) {
|
|
109
112
|
this.pool = pool
|
|
110
113
|
// If cancel() already raced in before the pool was attached, honor it.
|
|
111
|
-
if (
|
|
114
|
+
if (workerStop.get()) pool.shutdownNow()
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
/**
|
|
@@ -121,6 +124,7 @@ class ConcurrentRangeDownloader(
|
|
|
121
124
|
*/
|
|
122
125
|
fun cancel() {
|
|
123
126
|
aborted.set(true)
|
|
127
|
+
workerStop.set(true)
|
|
124
128
|
pool?.let { p ->
|
|
125
129
|
p.shutdownNow()
|
|
126
130
|
try {
|
|
@@ -221,9 +225,9 @@ class ConcurrentRangeDownloader(
|
|
|
221
225
|
)
|
|
222
226
|
onProgress(transferred.get(), total)
|
|
223
227
|
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
val aborted = cancelHandle?.
|
|
228
|
+
// External cancel also stops workers; internal fallback/failure must not
|
|
229
|
+
// mark the caller's explicit-cancellation flag.
|
|
230
|
+
val aborted = cancelHandle?.workerStop ?: AtomicBoolean(false)
|
|
227
231
|
val fallback = AtomicBoolean(false)
|
|
228
232
|
val firstError = AtomicReference<Exception?>(null)
|
|
229
233
|
|
|
@@ -243,11 +247,11 @@ class ConcurrentRangeDownloader(
|
|
|
243
247
|
}
|
|
244
248
|
} catch (e: FallbackException) {
|
|
245
249
|
fallback.set(true)
|
|
246
|
-
aborted.set(true)
|
|
247
250
|
firstError.compareAndSet(null, e)
|
|
248
|
-
} catch (e: Exception) {
|
|
249
251
|
aborted.set(true)
|
|
252
|
+
} catch (e: Exception) {
|
|
250
253
|
firstError.compareAndSet(null, e)
|
|
254
|
+
aborted.set(true)
|
|
251
255
|
}
|
|
252
256
|
}
|
|
253
257
|
}
|
|
@@ -274,7 +278,7 @@ class ConcurrentRangeDownloader(
|
|
|
274
278
|
.firstOrNull { segFile(it.index).length() != it.length }
|
|
275
279
|
if (incomplete != null) {
|
|
276
280
|
// Keep the segment files for the next attempt to resume.
|
|
277
|
-
throw
|
|
281
|
+
throw ProtocolException("Concurrent download incomplete (segment ${incomplete.index})")
|
|
278
282
|
}
|
|
279
283
|
|
|
280
284
|
// All segments complete → assemble the `.partial`.
|
|
@@ -293,6 +293,67 @@ class ConcurrentRangeDownloaderOcdsTest {
|
|
|
293
293
|
assertNoArtifactsLeft(partial)
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
+
@Test
|
|
297
|
+
fun internalFallbackDoesNotMarkCallerCancelled() {
|
|
298
|
+
val target = parts[2]
|
|
299
|
+
startServer(RangeFaultServer(content).arm(
|
|
300
|
+
RangeFaultServer.FaultMode.STATUS_200, target.start, target.end, times = Int.MAX_VALUE,
|
|
301
|
+
))
|
|
302
|
+
val handle = ConcurrentRangeDownloader.CancelHandle()
|
|
303
|
+
|
|
304
|
+
val outcome = newDownloader().download(url(), partialPath(), handle) { _, _ -> }
|
|
305
|
+
|
|
306
|
+
assertEquals(ConcurrentRangeDownloader.Outcome.FALLBACK, outcome)
|
|
307
|
+
assertFalse(handle.aborted.get())
|
|
308
|
+
assertNoArtifactsLeft(partialPath())
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
@Test
|
|
312
|
+
fun segment403PreservesHttpStatusWithoutMarkingCallerCancelled() {
|
|
313
|
+
val target = parts[2]
|
|
314
|
+
val normal = RangeFaultServer(content)
|
|
315
|
+
server = MockWebServer()
|
|
316
|
+
server.dispatcher = object : Dispatcher() {
|
|
317
|
+
override fun dispatch(request: RecordedRequest): MockResponse =
|
|
318
|
+
if (request.getHeader("Range") == "bytes=${target.start}-${target.end}") {
|
|
319
|
+
MockResponse().setResponseCode(403)
|
|
320
|
+
} else {
|
|
321
|
+
normal.dispatcher.dispatch(request)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
server.start()
|
|
325
|
+
val handle = ConcurrentRangeDownloader.CancelHandle()
|
|
326
|
+
|
|
327
|
+
val failure = runCatching {
|
|
328
|
+
newDownloader().download(url(), partialPath(), handle) { _, _ -> }
|
|
329
|
+
}.exceptionOrNull()
|
|
330
|
+
|
|
331
|
+
assertTrue(failure is ConcurrentRangeDownloader.PermanentHttpException)
|
|
332
|
+
assertEquals(403, (failure as ConcurrentRangeDownloader.PermanentHttpException).code)
|
|
333
|
+
assertFalse(handle.aborted.get())
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
@Test
|
|
337
|
+
fun exhaustedSocketTimeoutDoesNotMarkCallerCancelled() {
|
|
338
|
+
val target = parts[2]
|
|
339
|
+
startServer(RangeFaultServer(content).arm(
|
|
340
|
+
RangeFaultServer.FaultMode.STALL, target.start, target.end, times = Int.MAX_VALUE,
|
|
341
|
+
))
|
|
342
|
+
val handle = ConcurrentRangeDownloader.CancelHandle()
|
|
343
|
+
val client = OkHttpClient.Builder().readTimeout(200, TimeUnit.MILLISECONDS).build()
|
|
344
|
+
val downloader = ConcurrentRangeDownloader(client, maxPartRetry = 0)
|
|
345
|
+
|
|
346
|
+
val failure = runCatching {
|
|
347
|
+
downloader.download(url(), partialPath(), handle) { _, _ -> }
|
|
348
|
+
}.exceptionOrNull()
|
|
349
|
+
|
|
350
|
+
assertTrue(
|
|
351
|
+
"expected a socket timeout, got ${failure?.javaClass?.name}: ${failure?.message}",
|
|
352
|
+
failure is java.net.SocketTimeoutException,
|
|
353
|
+
)
|
|
354
|
+
assertFalse(handle.aborted.get())
|
|
355
|
+
}
|
|
356
|
+
|
|
296
357
|
// ------------------------------------------------------------------------
|
|
297
358
|
// OCDS-T4: 429 (+Retry-After: 0) once on a segment → transient retry succeeds.
|
|
298
359
|
// SHA correct; the affected segment requested twice; no from-0 restart.
|
|
@@ -436,17 +497,17 @@ class ConcurrentRangeDownloaderOcdsTest {
|
|
|
436
497
|
val partial = partialPath()
|
|
437
498
|
// First invocation: the short body leaves segment 6 one byte short, so this
|
|
438
499
|
// call throws "incomplete" and keeps the `.segN` for resume.
|
|
439
|
-
|
|
440
|
-
|
|
500
|
+
val handle = ConcurrentRangeDownloader.CancelHandle()
|
|
501
|
+
val firstFailure = runCatching {
|
|
441
502
|
newDownloader().download(
|
|
442
503
|
url = url(),
|
|
443
504
|
partialFilePath = partial,
|
|
505
|
+
cancelHandle = handle,
|
|
444
506
|
onProgress = { _, _ -> },
|
|
445
507
|
)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
assertTrue("short body must leave the first invocation incomplete", firstThrew)
|
|
508
|
+
}.exceptionOrNull()
|
|
509
|
+
assertTrue("short body must preserve a typed network failure", firstFailure is java.net.ProtocolException)
|
|
510
|
+
assertFalse("internal short body must not mark explicit cancellation", handle.aborted.get())
|
|
450
511
|
// The short segment is kept on disk, one byte under its planned length.
|
|
451
512
|
val seg = segFile(partial, target.index)
|
|
452
513
|
assertTrue("short `.segN` must be kept for resume", seg.exists())
|
|
@@ -532,6 +593,7 @@ class ConcurrentRangeDownloaderOcdsTest {
|
|
|
532
593
|
worker.shutdownNow()
|
|
533
594
|
|
|
534
595
|
assertTrue("cancel must abort promptly (took ${elapsed}ms)", elapsed < 12_000)
|
|
596
|
+
assertTrue("explicit cancellation must mark caller intent", handle.aborted.get())
|
|
535
597
|
|
|
536
598
|
// cancel-then-delete: now that workers are terminated, deleting the artifacts
|
|
537
599
|
// must STICK — no straggler worker resurrects a `.segN`.
|
package/package.json
CHANGED