Attempt to recover from HTTP 400 errors

pull/3782/head^2
Alex Baker 4 months ago
parent d5cda9e84b
commit ad616472b3

@ -7,6 +7,7 @@ import com.google.api.services.tasks.model.Task
import com.google.api.services.tasks.model.TaskList import com.google.api.services.tasks.model.TaskList
import com.google.api.services.tasks.model.TaskLists import com.google.api.services.tasks.model.TaskLists
import org.tasks.googleapis.BaseInvoker import org.tasks.googleapis.BaseInvoker
import timber.log.Timber
import java.io.IOException import java.io.IOException
/** /**
@ -43,21 +44,30 @@ class GtasksInvoker(
@Throws(IOException::class) @Throws(IOException::class)
suspend fun getAllPositions( suspend fun getAllPositions(
listId: String?, pageToken: String?): com.google.api.services.tasks.model.Tasks? = listId: String?,
execute( pageToken: String?,
service!! ): com.google.api.services.tasks.model.Tasks? =
.tasks() execute(
.list(listId) service!!
.setMaxResults(100) .tasks()
.setShowDeleted(false) .list(listId)
.setShowHidden(false) .setMaxResults(100)
.setPageToken(pageToken) .setShowDeleted(false)
.setFields("items(id,parent,position),nextPageToken")) .setShowHidden(false)
.setPageToken(pageToken)
.setFields("items(id,parent,position),nextPageToken")
)
@Throws(IOException::class) @Throws(IOException::class)
suspend fun createGtask( suspend fun createGtask(
listId: String?, task: Task?, parent: String?, previous: String?): Task? = listId: String?,
execute(service!!.tasks().insert(listId, task).setParent(parent).setPrevious(previous)) task: Task?,
parent: String?,
previous: String?,
): Task? {
Timber.d("createGtask(listId=$listId, task=<redacted>, parent=$parent, previous=$previous)")
return execute(service!!.tasks().insert(listId, task).setParent(parent).setPrevious(previous))
}
@Throws(IOException::class) @Throws(IOException::class)
suspend fun updateGtask(listId: String?, task: Task) = suspend fun updateGtask(listId: String?, task: Task) =
@ -65,19 +75,26 @@ class GtasksInvoker(
@Throws(IOException::class) @Throws(IOException::class)
suspend fun moveGtask( suspend fun moveGtask(
listId: String?, taskId: String?, parentId: String?, previousId: String?): Task? = listId: String?,
execute( taskId: String?,
service!! parentId: String?,
.tasks() previousId: String?,
.move(listId, taskId) ): Task? {
.setParent(parentId) Timber.d("moveGtask(listId=$listId, taskId=$taskId, parentId=$parentId, previousId=$previousId)")
.setPrevious(previousId)) return execute(
service!!
.tasks()
.move(listId, taskId)
.setParent(parentId)
.setPrevious(previousId)
)
}
@Throws(IOException::class) @Throws(IOException::class)
suspend fun deleteGtaskList(listId: String?) { suspend fun deleteGtaskList(listId: String?) {
try { try {
execute(service!!.tasklists().delete(listId)) execute(service!!.tasklists().delete(listId))
} catch (ignored: HttpNotFoundException) { } catch (_: HttpNotFoundException) {
} }
} }
@ -91,9 +108,10 @@ class GtasksInvoker(
@Throws(IOException::class) @Throws(IOException::class)
suspend fun deleteGtask(listId: String?, taskId: String?) { suspend fun deleteGtask(listId: String?, taskId: String?) {
Timber.d("deleteGtask(listId=$listId, taskId=$taskId)")
try { try {
execute(service!!.tasks().delete(listId, taskId)) execute(service!!.tasks().delete(listId, taskId))
} catch (ignored: HttpNotFoundException) { } catch (_: HttpNotFoundException) {
} }
} }
} }

@ -15,6 +15,7 @@ import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.service.TaskCreator.Companion.getDefaultAlarms import com.todoroo.astrid.service.TaskCreator.Companion.getDefaultAlarms
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.delay
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
@ -125,7 +126,21 @@ class GoogleTaskSynchronizer @Inject constructor(
preferences.setString(R.string.p_default_list, null) preferences.setString(R.string.p_default_list, null)
} }
} }
pushLocalChanges(account, gtasksInvoker) val failedTasks = mutableSetOf<Long>()
var retryTaskId = pushLocalChanges(account, gtasksInvoker)
while (retryTaskId != null) {
if (failedTasks.contains(retryTaskId)) {
throw IOException("Invalid Task ID: $retryTaskId")
}
failedTasks.add(retryTaskId)
Timber.d("Retrying push local changes due to stale task ID $retryTaskId (${failedTasks.size} total failed tasks)")
delay(1000)
retryTaskId = pushLocalChanges(account, gtasksInvoker)
}
for (list in caldavDao.getCalendarsByAccount(account.uuid!!)) { for (list in caldavDao.getCalendarsByAccount(account.uuid!!)) {
if (isNullOrEmpty(list.uuid)) { if (isNullOrEmpty(list.uuid)) {
firebase.reportException(RuntimeException("Empty remote id")) firebase.reportException(RuntimeException("Empty remote id"))
@ -164,15 +179,19 @@ class GoogleTaskSynchronizer @Inject constructor(
} }
@Throws(IOException::class) @Throws(IOException::class)
private suspend fun pushLocalChanges(account: CaldavAccount, gtasksInvoker: GtasksInvoker) { private suspend fun pushLocalChanges(account: CaldavAccount, gtasksInvoker: GtasksInvoker): Long? {
val tasks = taskDao.getGoogleTasksToPush(account.uuid!!) val tasks = taskDao.getGoogleTasksToPush(account.uuid!!)
for (task in tasks) { for (task in tasks) {
pushTask(task, gtasksInvoker) val staleTaskId = pushTask(task, gtasksInvoker)
if (staleTaskId != null) {
return staleTaskId
}
} }
return null
} }
@Throws(IOException::class) @Throws(IOException::class)
private suspend fun pushTask(task: org.tasks.data.entity.Task, gtasksInvoker: GtasksInvoker) { private suspend fun pushTask(task: org.tasks.data.entity.Task, gtasksInvoker: GtasksInvoker): Long? {
for (deleted in googleTaskDao.getDeletedByTaskId(task.id)) { for (deleted in googleTaskDao.getDeletedByTaskId(task.id)) {
deleted.remoteId?.let { deleted.remoteId?.let {
try { try {
@ -186,7 +205,7 @@ class GoogleTaskSynchronizer @Inject constructor(
} }
googleTaskDao.delete(deleted) googleTaskDao.delete(deleted)
} }
val gtasksMetadata = googleTaskDao.getByTaskId(task.id) ?: return val gtasksMetadata = googleTaskDao.getByTaskId(task.id) ?: return null
val remoteModel = Task() val remoteModel = Task()
var newlyCreated = false var newlyCreated = false
val remoteId: String? val remoteId: String?
@ -207,7 +226,7 @@ class GoogleTaskSynchronizer @Inject constructor(
// creating a task which may end up being cancelled. Also don't sync new but already // creating a task which may end up being cancelled. Also don't sync new but already
// deleted tasks // deleted tasks
if (newlyCreated && (isNullOrEmpty(task.title) || task.deletionDate > 0)) { if (newlyCreated && (isNullOrEmpty(task.title) || task.deletionDate > 0)) {
return return null
} }
// Update the remote model's changed properties // Update the remote model's changed properties
@ -230,10 +249,11 @@ class GoogleTaskSynchronizer @Inject constructor(
val parent = task.parent val parent = task.parent
val localParent = if (parent > 0) googleTaskDao.getRemoteId(parent) else null val localParent = if (parent > 0) googleTaskDao.getRemoteId(parent) else null
val previous = googleTaskDao.getPrevious( val previous = googleTaskDao.getPrevious(
listId!!, if (isNullOrEmpty(localParent)) 0 else parent, task.order ?: 0) listId, if (isNullOrEmpty(localParent)) 0 else parent, task.order ?: 0)
val created: Task? = try { val created: Task? = try {
gtasksInvoker.createGtask(listId, remoteModel, localParent, previous) gtasksInvoker.createGtask(listId, remoteModel, localParent, previous)
} catch (e: HttpNotFoundException) { } catch (e: HttpNotFoundException) {
Timber.e(e, "Failed to create task, retry without parent or order")
gtasksInvoker.createGtask(listId, remoteModel, null, null) gtasksInvoker.createGtask(listId, remoteModel, null, null)
} }
if (created != null) { if (created != null) {
@ -241,8 +261,10 @@ class GoogleTaskSynchronizer @Inject constructor(
gtasksMetadata.remoteId = created.id gtasksMetadata.remoteId = created.id
gtasksMetadata.calendar = listId gtasksMetadata.calendar = listId
setOrderAndParent(gtasksMetadata, created, task) setOrderAndParent(gtasksMetadata, created, task)
Timber.d("Created new task: $gtasksMetadata")
} else { } else {
return Timber.e("Empty response when creating task")
return null
} }
} else { } else {
try { try {
@ -251,29 +273,64 @@ class GoogleTaskSynchronizer @Inject constructor(
val parent = task.parent val parent = task.parent
val localParent = if (parent > 0) googleTaskDao.getRemoteId(parent) else null val localParent = if (parent > 0) googleTaskDao.getRemoteId(parent) else null
val previous = googleTaskDao.getPrevious( val previous = googleTaskDao.getPrevious(
listId!!, listId,
if (localParent.isNullOrBlank()) 0 else parent, if (localParent.isNullOrBlank()) 0 else parent,
task.order ?: 0) task.order ?: 0,
)
gtasksInvoker gtasksInvoker
.moveGtask(listId, remoteModel.id, localParent, previous) .moveGtask(
?.let { setOrderAndParent(gtasksMetadata, it, task) } listId = listId,
taskId = remoteModel.id,
parentId = localParent,
previousId = previous,
)
?.let {
setOrderAndParent(
googleTask = gtasksMetadata,
task = it,
local = task,
)
}
} catch (e: GoogleJsonResponseException) { } catch (e: GoogleJsonResponseException) {
if (e.statusCode == 400) { if (e.statusCode == 400) {
Timber.e(e) Timber.w("HTTP 400: clearing parent and order")
firebase.reportException(e)
taskDao.setParent(0L, listOf(task.id))
taskDao.setOrder(task.id, 0L)
googleTaskDao.update(gtasksMetadata.copy(isMoved = false))
return task.id
} else { } else {
throw e throw e
} }
} }
} }
// TODO: don't updateGtask if it was only moved // TODO: don't updateGtask if it was only moved
gtasksInvoker.updateGtask(listId, remoteModel) try {
} catch (e: HttpNotFoundException) { gtasksInvoker.updateGtask(listId, remoteModel)
} catch (e: GoogleJsonResponseException) {
if (e.statusCode == 400 && e.details?.message == "Invalid task ID") {
Timber.w("HTTP 400: Invalid task ID for ${remoteModel.id}, clearing to recreate on next sync")
firebase.reportException(e)
googleTaskDao.update(
gtasksMetadata.copy(
remoteId = "",
isMoved = false,
)
)
return task.id
} else {
throw e
}
}
} catch (_: HttpNotFoundException) {
Timber.w("HTTP 404, deleting $gtasksMetadata")
googleTaskDao.delete(gtasksMetadata) googleTaskDao.delete(gtasksMetadata)
return return null
} }
} }
gtasksMetadata.isMoved = false gtasksMetadata.isMoved = false
write(task, gtasksMetadata) write(task, gtasksMetadata)
return null
} }
@Throws(IOException::class) @Throws(IOException::class)

Loading…
Cancel
Save