Retry tracker after errors
This commit is contained in:
@@ -67,11 +67,15 @@ class Tracker @Inject constructor(
|
|||||||
return result
|
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() {
|
suspend fun gc() {
|
||||||
repository.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 manga = mangaRepositoryFactory.create(track.manga.source).getDetails(track.manga)
|
||||||
val updates = compare(track, manga, getBranch(manga))
|
val updates = compare(track, manga, getBranch(manga))
|
||||||
if (commit) {
|
if (commit) {
|
||||||
@@ -103,24 +107,24 @@ class Tracker @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* The main functionality of tracker: check new chapters in [manga] comparing to the [track]
|
* 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()) {
|
if (track.isEmpty()) {
|
||||||
// first check or manga was empty on last check
|
// 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 chapters = requireNotNull(manga.getChapters(branch))
|
||||||
val newChapters = chapters.takeLastWhile { x -> x.id != track.lastChapterId }
|
val newChapters = chapters.takeLastWhile { x -> x.id != track.lastChapterId }
|
||||||
return when {
|
return when {
|
||||||
newChapters.isEmpty() -> {
|
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 -> {
|
newChapters.size == chapters.size -> {
|
||||||
MangaUpdates(manga, emptyList(), isValid = false)
|
MangaUpdates.Success(manga, emptyList(), isValid = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
MangaUpdates(manga, newChapters, isValid = true)
|
MangaUpdates.Success(manga, newChapters, isValid = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class TrackingRepository @Inject constructor(
|
|||||||
db.trackLogsDao.gc()
|
db.trackLogsDao.gc()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveUpdates(updates: MangaUpdates) {
|
suspend fun saveUpdates(updates: MangaUpdates.Success) {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
val track = getOrCreateTrack(updates.manga.id).mergeWith(updates)
|
val track = getOrCreateTrack(updates.manga.id).mergeWith(updates)
|
||||||
db.tracksDao.upsert(track)
|
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 history = db.historyDao.find(updates.manga.id) ?: return
|
||||||
val chapters = updates.manga.chapters
|
val chapters = updates.manga.chapters
|
||||||
if (chapters.isNullOrEmpty()) {
|
if (chapters.isNullOrEmpty()) {
|
||||||
@@ -214,7 +214,7 @@ class TrackingRepository @Inject constructor(
|
|||||||
db.historyDao.update(history.copy(percent = newPercent))
|
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()
|
val chapters = updates.manga.chapters.orEmpty()
|
||||||
return TrackEntity(
|
return TrackEntity(
|
||||||
mangaId = mangaId,
|
mangaId = mangaId,
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
package org.koitharu.kotatsu.tracker.domain.model
|
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.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
|
||||||
class MangaUpdates(
|
sealed interface MangaUpdates {
|
||||||
val manga: Manga,
|
|
||||||
val newChapters: List<MangaChapter>,
|
|
||||||
val isValid: Boolean,
|
|
||||||
) {
|
|
||||||
|
|
||||||
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.NotificationManagerCompat
|
||||||
import androidx.core.app.PendingIntentCompat
|
import androidx.core.app.PendingIntentCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.hilt.work.HiltWorker
|
import androidx.hilt.work.HiltWorker
|
||||||
import androidx.lifecycle.asFlow
|
import androidx.lifecycle.asFlow
|
||||||
import androidx.work.BackoffPolicy
|
import androidx.work.BackoffPolicy
|
||||||
@@ -32,12 +33,14 @@ import coil.request.ImageRequest
|
|||||||
import dagger.Reusable
|
import dagger.Reusable
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import kotlinx.coroutines.withContext
|
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.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
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.parsers.util.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
|
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
|
||||||
import org.koitharu.kotatsu.tracker.domain.Tracker
|
import org.koitharu.kotatsu.tracker.domain.Tracker
|
||||||
@@ -75,7 +79,7 @@ class TrackWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
trySetForeground()
|
trySetForeground()
|
||||||
logger.log("doWork()")
|
logger.log("doWork(): attempt $runAttemptCount")
|
||||||
return try {
|
return try {
|
||||||
doWorkImpl()
|
doWorkImpl()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -93,7 +97,12 @@ class TrackWorker @AssistedInject constructor(
|
|||||||
if (!settings.isTrackerEnabled) {
|
if (!settings.isTrackerEnabled) {
|
||||||
return Result.success(workDataOf(0, 0))
|
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")
|
logger.log("Total ${tracks.size} tracks")
|
||||||
if (tracks.isEmpty()) {
|
if (tracks.isEmpty()) {
|
||||||
return Result.success(workDataOf(0, 0))
|
return Result.success(workDataOf(0, 0))
|
||||||
@@ -104,23 +113,32 @@ class TrackWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
var success = 0
|
var success = 0
|
||||||
var failed = 0
|
var failed = 0
|
||||||
|
val retry = HashSet<Long>()
|
||||||
results.forEach { x ->
|
results.forEach { x ->
|
||||||
if (x == null) {
|
when (x) {
|
||||||
failed++
|
is MangaUpdates.Success -> success++
|
||||||
} else {
|
is MangaUpdates.Failure -> {
|
||||||
success++
|
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)
|
val resultData = workDataOf(success, failed)
|
||||||
return if (success == 0 && failed != 0) {
|
return when {
|
||||||
Result.failure(resultData)
|
retry.isNotEmpty() -> Result.retry()
|
||||||
} else {
|
success == 0 && failed != 0 -> Result.failure(resultData)
|
||||||
Result.success(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)
|
val semaphore = Semaphore(MAX_PARALLELISM)
|
||||||
return channelFlow {
|
return channelFlow {
|
||||||
for ((track, channelId) in tracks) {
|
for ((track, channelId) in tracks) {
|
||||||
@@ -142,7 +160,12 @@ class TrackWorker @AssistedInject constructor(
|
|||||||
newChapters = updates.newChapters,
|
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)
|
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 {
|
private fun workDataOf(success: Int, failed: Int): Data {
|
||||||
return Data.Builder()
|
return Data.Builder()
|
||||||
.putInt(DATA_KEY_SUCCESS, success)
|
.putInt(DATA_KEY_SUCCESS, success)
|
||||||
@@ -256,7 +295,7 @@ class TrackWorker @AssistedInject constructor(
|
|||||||
val request = PeriodicWorkRequestBuilder<TrackWorker>(4, TimeUnit.HOURS)
|
val request = PeriodicWorkRequestBuilder<TrackWorker>(4, TimeUnit.HOURS)
|
||||||
.setConstraints(constraints)
|
.setConstraints(constraints)
|
||||||
.addTag(TAG)
|
.addTag(TAG)
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
|
.setBackoffCriteria(BackoffPolicy.LINEAR, 5, TimeUnit.MINUTES)
|
||||||
.build()
|
.build()
|
||||||
workManager
|
workManager
|
||||||
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request)
|
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||||
@@ -306,7 +345,9 @@ class TrackWorker @AssistedInject constructor(
|
|||||||
const val TAG = "tracking"
|
const val TAG = "tracking"
|
||||||
const val TAG_ONESHOT = "tracking_oneshot"
|
const val TAG_ONESHOT = "tracking_oneshot"
|
||||||
const val MAX_PARALLELISM = 3
|
const val MAX_PARALLELISM = 3
|
||||||
|
const val MAX_ATTEMPTS = 4
|
||||||
const val DATA_KEY_SUCCESS = "success"
|
const val DATA_KEY_SUCCESS = "success"
|
||||||
const val DATA_KEY_FAILED = "failed"
|
const val DATA_KEY_FAILED = "failed"
|
||||||
|
const val KEY_RETRY_IDS = "retry"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user