Skip to content

Commit

Permalink
Possible fix for #458 (retry on cancellation)
Browse files Browse the repository at this point in the history
Overrides statusUpdate to 'canceled' if task was 'manually' cancelled, even if teh WorkManager generates a 'failed' status.
  • Loading branch information
781flyingdutchman committed Feb 22, 2025
1 parent 18f185c commit 1ecacca
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ class BDPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
var requireWifi = RequireWiFi.asSetByTask // global setting
val localResumeData =
mutableMapOf<String, ResumeData>() // by taskId, for pause notifications
var canceledTaskIds = mutableMapOf<String, Long>() // <taskId, timeMillis>
var cancelUpdateSentForTaskId = mutableMapOf<String, Long>() // <taskId, timeMillis>
val pausedTaskIds = mutableSetOf<String>() // <taskId>, acts as flag
val canceledTaskIds = mutableSetOf<String>() // <taskId>, acts as flag
val parallelDownloadTaskWorkers = HashMap<String, ParallelDownloadTaskWorker>()
val tasksToReEnqueue = mutableSetOf<Task>() // for when WiFi requirement changes
val taskIdsRequiringWiFi =
Expand Down Expand Up @@ -121,9 +122,9 @@ class BDPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
} else {
Log.w(TAG, "Could not find backgroundChannel for taskId ${task.taskId}")
}
// store host if we have a HoldingQueue
holdingQueue?.hostByTaskId?.set(task.taskId, task.host())
canceledTaskIds.remove(task.taskId)
holdingQueue?.hostByTaskId?.set(task.taskId, task.host()) // store host if we have a HoldingQueue
canceledTaskIds.remove(task.taskId) // reset flag
cancelUpdateSentForTaskId.remove(task.taskId) // reset flag
val dataBuilder = Data.Builder().putString(TaskWorker.keyTask, taskToJsonString(task))
if (notificationConfigJsonString != null) {
dataBuilder.putString(
Expand Down Expand Up @@ -237,13 +238,15 @@ class BDPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
): Boolean {
// cancel chunk tasks if this is a ParallelDownloadTask
parallelDownloadTaskWorkers[taskId]?.cancelAllChunkTasks()
// get the workInfos for the task (should be only one)
val workInfos = withContext(Dispatchers.IO) {
workManager.getWorkInfosByTag("taskId=$taskId").get()
}
if (workInfos.isEmpty()) {
Log.d(TAG, "Could not find tasks to cancel")
return false
}
// send cancel update and remove notification
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val tasksMap = getTaskMap(prefs)
for (workInfo in workInfos) {
Expand All @@ -252,6 +255,7 @@ class BDPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
// and remove associated notification
val task = tasksMap[taskId]
if (task != null) {
canceledTaskIds.add(task.taskId)
processStatusUpdate(
task,
TaskStatus.canceled,
Expand Down Expand Up @@ -554,6 +558,7 @@ class BDPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
val taskId = tags.first().substring(7)
val task = tasksMap[taskId]
if (task != null) {
canceledTaskIds.add(task.taskId)
processStatusUpdate(
task,
TaskStatus.canceled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.bbflight.background_downloader.TaskWorker.Companion.TAG
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -121,13 +120,17 @@ open class TaskWorker(

// Normal status update

// Check if the task was 'manually' canceled, in which case a TaskStatus.failed may be
// incorrectly generated by the WorkManager, so we change it to 'canceled'
val modifiedStatus = if (BDPlugin.canceledTaskIds.contains(task.taskId)) TaskStatus.canceled else status

// A 'failed' progress update is only provided if
// a retry is not needed: if it is needed, a `waitingToRetry` progress update
// will be generated on the Dart side
val retryNeeded = status == TaskStatus.failed && task.retriesRemaining > 0
val retryNeeded = modifiedStatus == TaskStatus.failed && task.retriesRemaining > 0
var canSendStatusUpdate = true // may become false for cancellations
// if task is in final state, process a final progressUpdate
when (status) {
when (modifiedStatus) {
TaskStatus.complete -> processProgressUpdate(
task, 1.0, prefs
)
Expand All @@ -139,7 +142,7 @@ open class TaskWorker(
TaskStatus.canceled -> {
canSendStatusUpdate = canSendCancellation(task)
if (canSendStatusUpdate) {
BDPlugin.canceledTaskIds[task.taskId] =
BDPlugin.cancelUpdateSentForTaskId[task.taskId] =
currentTimeMillis()
processProgressUpdate(
task, -2.0, prefs
Expand All @@ -159,22 +162,22 @@ open class TaskWorker(
}

val taskStatusUpdate =
if (status.isFinalState()) { // last update gets all data
if (modifiedStatus.isFinalState()) { // last update gets all data
TaskStatusUpdate(
task = task,
taskStatus = status,
exception = if (status == TaskStatus.failed) taskException
taskStatus = modifiedStatus,
exception = if (modifiedStatus == TaskStatus.failed) taskException
?: TaskException(ExceptionType.general) else null,
responseBody = responseBody,
responseStatusCode = if (status == TaskStatus.complete || status == TaskStatus.notFound) responseStatusCode else null,
responseStatusCode = if (modifiedStatus == TaskStatus.complete || modifiedStatus == TaskStatus.notFound) responseStatusCode else null,
responseHeaders = responseHeaders?.filterNotNull()
?.mapKeys { it.key.lowercase() },
mimeType = mimeType,
charSet = charSet
)
} else TaskStatusUpdate( // interim updates are limited
task = task,
taskStatus = status,
taskStatus = modifiedStatus,
exception = null,
responseBody = null,
responseStatusCode = null,
Expand All @@ -186,6 +189,7 @@ open class TaskWorker(
// Post update if task expects one, or if failed and retry is needed
if (canSendStatusUpdate && (task.providesStatusUpdates() || retryNeeded)) {
val arg = taskStatusUpdate.argList
Log.wtf(TAG, "Posting status update for ${task.taskId}: ${taskStatusUpdate.taskStatus}")
postOnBackgroundChannel("statusUpdate", task, arg, onFail = {
// unsuccessful post, so store in local prefs
Log.d(TAG, "Could not post status update -> storing locally")
Expand All @@ -200,8 +204,8 @@ open class TaskWorker(
// if task is in final state, cancel the WorkManager job (if failed),
// remove task from persistent storage, clean up references to taskId
// and invoke the onTaskFinishedCallback if necessary
if (status.isFinalState()) {
if (status == TaskStatus.failed) {
if (modifiedStatus.isFinalState()) {
if (modifiedStatus == TaskStatus.failed) {
// Cancel the WorkManager job.
// This is to avoid the WorkManager restarting a job that was
// canceled because job constraints are violated (e.g. network unavailable)
Expand Down Expand Up @@ -240,13 +244,13 @@ open class TaskWorker(
*
* Cancellation can only be sent if it wasn't already sent by the [BDPlugin]
* in the cancelTasksWithId method. Side effect is to clean out older cancellation entries
* from the [BDPlugin.canceledTaskIds]
* from the [BDPlugin.cancelUpdateSentForTaskId]
*/
private fun canSendCancellation(task: Task): Boolean {
val now = currentTimeMillis()
BDPlugin.canceledTaskIds =
BDPlugin.canceledTaskIds.filter { now - it.value < 1000 } as MutableMap
return BDPlugin.canceledTaskIds[task.taskId] == null
BDPlugin.cancelUpdateSentForTaskId =
BDPlugin.cancelUpdateSentForTaskId.filter { now - it.value < 1000 } as MutableMap
return BDPlugin.cancelUpdateSentForTaskId[task.taskId] == null
}

/**
Expand Down Expand Up @@ -560,7 +564,7 @@ open class TaskWorker(
)

is CancellationException -> {
if (BDPlugin.canceledTaskIds.contains(task.taskId)) {
if (BDPlugin.cancelUpdateSentForTaskId.contains(task.taskId)) {
Log.i(
TAG,
"Canceled task with id ${task.taskId}: ${e.message}"
Expand Down

0 comments on commit 1ecacca

Please sign in to comment.