fix(download): recover queue after worker timeout (#524)

This commit is contained in:
zarzet
2026-08-11 17:23:29 +07:00
parent 1d363d5166
commit 95f2879110
6 changed files with 125 additions and 4 deletions
@@ -467,8 +467,58 @@ class DownloadService : Service() {
*/
override fun onTimeout(startId: Int, fgsType: Int) {
android.util.Log.w("DownloadService", "Foreground service timeout reached (6 hours limit). Stopping service.")
stopForegroundService()
stopNativeWorkerForSystemTimeout()
}
private fun stopNativeWorkerForSystemTimeout() {
if (!hasNativeWorkerState()) {
stopForegroundService()
return
}
nativeWorkerCancelRequested = true
// Supersede the coroutine before cancelling it. Its catch/finally
// blocks must not publish a skipped/finished state over the recovery
// snapshot written below.
nativeWorkerGeneration++
val activeItemIds = synchronized(nativeWorkerItems) {
nativeWorkerItems
.filter { NativeWorkerPolicy.statusAfterWorkerStop(it.status) != it.status }
.map { it.itemId }
}
for (itemId in activeItemIds) {
try {
Gobackend.cancelDownload(itemId)
} catch (_: Exception) {
}
}
NativeDownloadFinalizer.cancelActiveWork()
nativeWorkerJob?.cancel(
CancellationException("Native queue stopped by Android timeout")
)
synchronized(nativeWorkerItems) {
for (item in nativeWorkerItems) {
val recoveredStatus = NativeWorkerPolicy.statusAfterWorkerStop(item.status)
if (recoveredStatus == item.status) continue
item.status = recoveredStatus
item.progress = 0.0
item.bytesReceived = 0L
item.bytesTotal = 0L
item.error = ""
item.resultJson = null
}
}
nativeWorkerCurrentItemId = ""
writeNativeWorkerSnapshot(
isRunning = false,
isPaused = false,
currentItemId = "",
message = "Android background time limit reached",
includeItems = true,
)
// Cancellation and the final recovery snapshot were handled above.
// Only tear down the foreground-service resources here.
stopForegroundService(cancelNativeWorker = false)
}
private fun createNotificationChannel() {
@@ -981,7 +1031,9 @@ class DownloadService : Service() {
)
}
} catch (e: CancellationException) {
if (nativeWorkerCancelRequested) {
if (nativeWorkerCancelRequested &&
generation == nativeWorkerGeneration
) {
updateNativeWorkerItem(request.itemId) {
it.status = "skipped"
it.error = "Cancelled"
@@ -68,4 +68,9 @@ internal object NativeWorkerPolicy {
completed: Int,
failed: Int,
): Boolean = !cancelRequested && completed + failed > 0
fun statusAfterWorkerStop(status: String): String = when (status) {
"preparing", "downloading", "finalizing" -> "queued"
else -> status
}
}
@@ -149,4 +149,15 @@ class NativeWorkerPolicyTest {
),
)
}
@Test
fun stoppedWorkerRequeuesOnlyInFlightItems() {
listOf("preparing", "downloading", "finalizing").forEach { status ->
assertEquals("queued", NativeWorkerPolicy.statusAfterWorkerStop(status))
}
assertEquals("queued", NativeWorkerPolicy.statusAfterWorkerStop("queued"))
assertEquals("completed", NativeWorkerPolicy.statusAfterWorkerStop("completed"))
assertEquals("failed", NativeWorkerPolicy.statusAfterWorkerStop("failed"))
assertEquals("skipped", NativeWorkerPolicy.statusAfterWorkerStop("skipped"))
}
}