Retry tracker after errors
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user