Retry tracker after errors

This commit is contained in:
Koitharu
2023-08-12 13:55:13 +03:00
parent c83538f66d
commit 1c4bd6da28
4 changed files with 89 additions and 30 deletions

View File

@@ -67,11 +67,15 @@ class Tracker @Inject constructor(
return result
}
suspend fun getTracks(ids: Set<Long>): List<TrackingItem> {
return getAllTracks().filterTo(ArrayList(ids.size)) { x -> x.tracking.manga.id in ids }
}
suspend fun gc() {
repository.gc()
}
suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates {
suspend fun fetchUpdates(track: MangaTracking, commit: Boolean): MangaUpdates.Success {
val manga = mangaRepositoryFactory.create(track.manga.source).getDetails(track.manga)
val updates = compare(track, manga, getBranch(manga))
if (commit) {
@@ -103,24 +107,24 @@ class Tracker @Inject constructor(
/**
* The main functionality of tracker: check new chapters in [manga] comparing to the [track]
*/
private fun compare(track: MangaTracking, manga: Manga, branch: String?): MangaUpdates {
private fun compare(track: MangaTracking, manga: Manga, branch: String?): MangaUpdates.Success {
if (track.isEmpty()) {
// first check or manga was empty on last check
return MangaUpdates(manga, emptyList(), isValid = false)
return MangaUpdates.Success(manga, emptyList(), isValid = false)
}
val chapters = requireNotNull(manga.getChapters(branch))
val newChapters = chapters.takeLastWhile { x -> x.id != track.lastChapterId }
return when {
newChapters.isEmpty() -> {
MangaUpdates(manga, emptyList(), isValid = chapters.lastOrNull()?.id == track.lastChapterId)
MangaUpdates.Success(manga, emptyList(), isValid = chapters.lastOrNull()?.id == track.lastChapterId)
}
newChapters.size == chapters.size -> {
MangaUpdates(manga, emptyList(), isValid = false)
MangaUpdates.Success(manga, emptyList(), isValid = false)
}
else -> {
MangaUpdates(manga, newChapters, isValid = true)
MangaUpdates.Success(manga, newChapters, isValid = true)
}
}
}

View File

@@ -118,7 +118,7 @@ class TrackingRepository @Inject constructor(
db.trackLogsDao.gc()
}
suspend fun saveUpdates(updates: MangaUpdates) {
suspend fun saveUpdates(updates: MangaUpdates.Success) {
db.withTransaction {
val track = getOrCreateTrack(updates.manga.id).mergeWith(updates)
db.tracksDao.upsert(track)
@@ -199,7 +199,7 @@ class TrackingRepository @Inject constructor(
)
}
private suspend fun updatePercent(updates: MangaUpdates) {
private suspend fun updatePercent(updates: MangaUpdates.Success) {
val history = db.historyDao.find(updates.manga.id) ?: return
val chapters = updates.manga.chapters
if (chapters.isNullOrEmpty()) {
@@ -214,7 +214,7 @@ class TrackingRepository @Inject constructor(
db.historyDao.update(history.copy(percent = newPercent))
}
private fun TrackEntity.mergeWith(updates: MangaUpdates): TrackEntity {
private fun TrackEntity.mergeWith(updates: MangaUpdates.Success): TrackEntity {
val chapters = updates.manga.chapters.orEmpty()
return TrackEntity(
mangaId = mangaId,

View File

@@ -1,13 +1,27 @@
package org.koitharu.kotatsu.tracker.domain.model
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
class MangaUpdates(
val manga: Manga,
val newChapters: List<MangaChapter>,
val isValid: Boolean,
) {
sealed interface MangaUpdates {
fun isNotEmpty() = newChapters.isNotEmpty()
}
val manga: Manga
class Success(
override val manga: Manga,
val newChapters: List<MangaChapter>,
val isValid: Boolean,
) : MangaUpdates {
fun isNotEmpty() = newChapters.isNotEmpty()
}
class Failure(
override val manga: Manga,
val error: Throwable?,
) : MangaUpdates {
fun shouldRetry() = error is TooManyRequestExceptions
}
}

View File

@@ -10,6 +10,7 @@ import androidx.core.app.NotificationCompat.VISIBILITY_SECRET
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.hilt.work.HiltWorker
import androidx.lifecycle.asFlow
import androidx.work.BackoffPolicy
@@ -32,12 +33,14 @@ import coil.request.ImageRequest
import dagger.Reusable
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
@@ -54,6 +57,7 @@ import org.koitharu.kotatsu.core.util.ext.trySetForeground
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
import org.koitharu.kotatsu.tracker.domain.Tracker
@@ -75,7 +79,7 @@ class TrackWorker @AssistedInject constructor(
override suspend fun doWork(): Result {
trySetForeground()
logger.log("doWork()")
logger.log("doWork(): attempt $runAttemptCount")
return try {
doWorkImpl()
} catch (e: Throwable) {
@@ -93,7 +97,12 @@ class TrackWorker @AssistedInject constructor(
if (!settings.isTrackerEnabled) {
return Result.success(workDataOf(0, 0))
}
val tracks = tracker.getAllTracks()
val retryIds = getRetryIds()
val tracks = if (retryIds.isNotEmpty()) {
tracker.getTracks(retryIds)
} else {
tracker.getAllTracks()
}
logger.log("Total ${tracks.size} tracks")
if (tracks.isEmpty()) {
return Result.success(workDataOf(0, 0))
@@ -104,23 +113,32 @@ class TrackWorker @AssistedInject constructor(
var success = 0
var failed = 0
val retry = HashSet<Long>()
results.forEach { x ->
if (x == null) {
failed++
} else {
success++
when (x) {
is MangaUpdates.Success -> success++
is MangaUpdates.Failure -> {
failed++
if (x.shouldRetry()) {
retry += x.manga.id
}
}
}
}
logger.log("Result: success: $success, failed: $failed")
if (runAttemptCount > MAX_ATTEMPTS) {
retry.clear()
}
setRetryIds(retry)
logger.log("Result: success: $success, failed: $failed, retry: ${retry.size}")
val resultData = workDataOf(success, failed)
return if (success == 0 && failed != 0) {
Result.failure(resultData)
} else {
Result.success(resultData)
return when {
retry.isNotEmpty() -> Result.retry()
success == 0 && failed != 0 -> Result.failure(resultData)
else -> Result.success(resultData)
}
}
private suspend fun checkUpdatesAsync(tracks: List<TrackingItem>): List<MangaUpdates?> {
private suspend fun checkUpdatesAsync(tracks: List<TrackingItem>): List<MangaUpdates> {
val semaphore = Semaphore(MAX_PARALLELISM)
return channelFlow {
for ((track, channelId) in tracks) {
@@ -142,7 +160,12 @@ class TrackWorker @AssistedInject constructor(
newChapters = updates.newChapters,
)
}
}.getOrNull(),
}.getOrElse { error ->
MangaUpdates.Failure(
manga = track.manga,
error = error,
)
},
)
}
}
@@ -238,6 +261,22 @@ class TrackWorker @AssistedInject constructor(
return ForegroundInfo(WORKER_NOTIFICATION_ID, notification)
}
private suspend fun setRetryIds(ids: Set<Long>) = runInterruptible(Dispatchers.IO) {
val prefs = applicationContext.getSharedPreferences(TAG, Context.MODE_PRIVATE)
prefs.edit(commit = true) {
if (ids.isEmpty()) {
remove(KEY_RETRY_IDS)
} else {
putStringSet(KEY_RETRY_IDS, ids.mapToSet { it.toString() })
}
}
}
private fun getRetryIds(): Set<Long> {
val prefs = applicationContext.getSharedPreferences(TAG, Context.MODE_PRIVATE)
return prefs.getStringSet(KEY_RETRY_IDS, null)?.mapToSet { it.toLong() }.orEmpty()
}
private fun workDataOf(success: Int, failed: Int): Data {
return Data.Builder()
.putInt(DATA_KEY_SUCCESS, success)
@@ -256,7 +295,7 @@ class TrackWorker @AssistedInject constructor(
val request = PeriodicWorkRequestBuilder<TrackWorker>(4, TimeUnit.HOURS)
.setConstraints(constraints)
.addTag(TAG)
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
.setBackoffCriteria(BackoffPolicy.LINEAR, 5, TimeUnit.MINUTES)
.build()
workManager
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request)
@@ -306,7 +345,9 @@ class TrackWorker @AssistedInject constructor(
const val TAG = "tracking"
const val TAG_ONESHOT = "tracking_oneshot"
const val MAX_PARALLELISM = 3
const val MAX_ATTEMPTS = 4
const val DATA_KEY_SUCCESS = "success"
const val DATA_KEY_FAILED = "failed"
const val KEY_RETRY_IDS = "retry"
}
}