mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-26 12:52:40 +02:00
fix(download): recover queue after worker timeout (#524)
This commit is contained in:
@@ -467,8 +467,58 @@ class DownloadService : Service() {
|
|||||||
*/
|
*/
|
||||||
override fun onTimeout(startId: Int, fgsType: Int) {
|
override fun onTimeout(startId: Int, fgsType: Int) {
|
||||||
android.util.Log.w("DownloadService", "Foreground service timeout reached (6 hours limit). Stopping service.")
|
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() {
|
private fun createNotificationChannel() {
|
||||||
@@ -981,7 +1031,9 @@ class DownloadService : Service() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
if (nativeWorkerCancelRequested) {
|
if (nativeWorkerCancelRequested &&
|
||||||
|
generation == nativeWorkerGeneration
|
||||||
|
) {
|
||||||
updateNativeWorkerItem(request.itemId) {
|
updateNativeWorkerItem(request.itemId) {
|
||||||
it.status = "skipped"
|
it.status = "skipped"
|
||||||
it.error = "Cancelled"
|
it.error = "Cancelled"
|
||||||
|
|||||||
@@ -68,4 +68,9 @@ internal object NativeWorkerPolicy {
|
|||||||
completed: Int,
|
completed: Int,
|
||||||
failed: Int,
|
failed: Int,
|
||||||
): Boolean = !cancelRequested && completed + failed > 0
|
): 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"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,17 @@ bool canStartForegroundDownloadForLifecycle(AppLifecycleState? lifecycleState) {
|
|||||||
return lifecycleState == AppLifecycleState.resumed;
|
return lifecycleState == AppLifecycleState.resumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String nativeWorkerStatusAfterSnapshotStop({
|
||||||
|
required bool workerRunning,
|
||||||
|
required String status,
|
||||||
|
}) {
|
||||||
|
if (workerRunning) return status;
|
||||||
|
return switch (status) {
|
||||||
|
'preparing' || 'downloading' || 'finalizing' => 'queued',
|
||||||
|
_ => status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Keeps a download in its finalizing state until its durable Library record
|
/// Keeps a download in its finalizing state until its durable Library record
|
||||||
/// has been written. If persistence fails, completion is deliberately not
|
/// has been written. If persistence fails, completion is deliberately not
|
||||||
/// published so the queue can surface the error instead of losing the file
|
/// published so the queue can surface the error instead of losing the file
|
||||||
|
|||||||
@@ -863,6 +863,7 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
|||||||
if (itemSnapshots.isEmpty) {
|
if (itemSnapshots.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final workerRunning = snapshot['is_running'] == true;
|
||||||
|
|
||||||
for (final itemSnapshot in itemSnapshots) {
|
for (final itemSnapshot in itemSnapshots) {
|
||||||
final itemId = itemSnapshot['item_id']?.toString() ?? '';
|
final itemId = itemSnapshot['item_id']?.toString() ?? '';
|
||||||
@@ -872,7 +873,10 @@ extension _DownloadQueueNativeWorker on DownloadQueueNotifier {
|
|||||||
final context = contexts[itemId];
|
final context = contexts[itemId];
|
||||||
if (context == null) continue;
|
if (context == null) continue;
|
||||||
|
|
||||||
final status = itemSnapshot['status']?.toString() ?? 'queued';
|
final status = nativeWorkerStatusAfterSnapshotStop(
|
||||||
|
workerRunning: workerRunning,
|
||||||
|
status: itemSnapshot['status']?.toString() ?? 'queued',
|
||||||
|
);
|
||||||
final progress = ((itemSnapshot['progress'] as num?)?.toDouble() ?? 0.0)
|
final progress = ((itemSnapshot['progress'] as num?)?.toDouble() ?? 0.0)
|
||||||
.clamp(0.0, 1.0)
|
.clamp(0.0, 1.0)
|
||||||
.toDouble();
|
.toDouble();
|
||||||
|
|||||||
@@ -51,4 +51,42 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('native worker stop recovery', () {
|
||||||
|
test('requeues every in-flight snapshot state after the worker stops', () {
|
||||||
|
for (final status in const ['preparing', 'downloading', 'finalizing']) {
|
||||||
|
expect(
|
||||||
|
nativeWorkerStatusAfterSnapshotStop(
|
||||||
|
workerRunning: false,
|
||||||
|
status: status,
|
||||||
|
),
|
||||||
|
'queued',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves terminal states and live worker progress', () {
|
||||||
|
expect(
|
||||||
|
nativeWorkerStatusAfterSnapshotStop(
|
||||||
|
workerRunning: false,
|
||||||
|
status: 'completed',
|
||||||
|
),
|
||||||
|
'completed',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
nativeWorkerStatusAfterSnapshotStop(
|
||||||
|
workerRunning: false,
|
||||||
|
status: 'failed',
|
||||||
|
),
|
||||||
|
'failed',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
nativeWorkerStatusAfterSnapshotStop(
|
||||||
|
workerRunning: true,
|
||||||
|
status: 'downloading',
|
||||||
|
),
|
||||||
|
'downloading',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user