Feature/improve automated backup (#597)

* Add option to disable cleanup of backups

* Ensure the minimum TTL of backups to 1 day

* Schedule the automated backup on a specific time of the day

* Introduce scheduler that takes system hibernation time into account

In case the system was hibernating/suspended scheduled task that should have been executed during that time would not get triggered and thus, miss an execution.

To prevent this, this new scheduler periodically checks if the system was suspended and in case it was, triggers any task that missed its last execution

* Use new scheduler
This commit is contained in:
schroda
2023-07-20 23:47:30 +02:00
committed by GitHub
parent 0338ac3810
commit c1d702a51c
8 changed files with 203 additions and 50 deletions

View File

@@ -36,13 +36,12 @@ import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.manga.model.table.toDataClass
import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.util.HAScheduler
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.TimeUnit
import java.util.prefs.Preferences
import kotlin.time.Duration.Companion.days
@@ -50,40 +49,37 @@ import kotlin.time.Duration.Companion.days
object ProtoBackupExport : ProtoBackupBase() {
private val logger = KotlinLogging.logger { }
private val applicationDirs by DI.global.instance<ApplicationDirs>()
private var backupSchedulerJobId: String = ""
private const val lastAutomatedBackupKey = "lastAutomatedBackupKey"
private val preferences = Preferences.userNodeForPackage(ProtoBackupExport::class.java)
private val backupTimer = Timer()
private var currentAutomatedBackupTask: TimerTask? = null
fun scheduleAutomatedBackupTask() {
HAScheduler.deschedule(backupSchedulerJobId)
if (!serverConfig.automatedBackups) {
currentAutomatedBackupTask?.cancel()
return
}
val minInterval = 1.days
val interval = serverConfig.backupInterval.days
val backupInterval = interval.coerceAtLeast(minInterval).inWholeMilliseconds
val lastAutomatedBackup = preferences.getLong(lastAutomatedBackupKey, 0)
val initialDelay =
backupInterval - (System.currentTimeMillis() - lastAutomatedBackup) % backupInterval
currentAutomatedBackupTask?.cancel()
currentAutomatedBackupTask = object : TimerTask() {
override fun run() {
cleanupAutomatedBackups()
createAutomatedBackup()
preferences.putLong(lastAutomatedBackupKey, System.currentTimeMillis())
}
val task = {
cleanupAutomatedBackups()
createAutomatedBackup()
preferences.putLong(lastAutomatedBackupKey, System.currentTimeMillis())
}
backupTimer.scheduleAtFixedRate(
currentAutomatedBackupTask,
initialDelay,
backupInterval
)
val (hour, minute) = serverConfig.backupTime.split(":").map { it.toInt() }
val backupHour = hour.coerceAtLeast(0).coerceAtMost(23)
val backupMinute = minute.coerceAtLeast(0).coerceAtMost(59)
val backupInterval = serverConfig.backupInterval.days.coerceAtLeast(1.days)
// trigger last backup in case the server wasn't running on the scheduled time
val lastAutomatedBackup = preferences.getLong(lastAutomatedBackupKey, System.currentTimeMillis())
val wasPreviousBackupTriggered =
(System.currentTimeMillis() - lastAutomatedBackup) < backupInterval.inWholeMilliseconds
if (!wasPreviousBackupTriggered) {
task()
}
HAScheduler.schedule(task, "$backupMinute $backupHour */${backupInterval.inWholeDays} * *", "backup")
}
private fun createAutomatedBackup() {
@@ -105,11 +101,15 @@ object ProtoBackupExport : ProtoBackupBase() {
backupFile.outputStream().use { output -> input.copyTo(output) }
}
}
private fun cleanupAutomatedBackups() {
logger.debug { "Cleanup automated backups" }
logger.debug { "Cleanup automated backups (ttl= ${serverConfig.backupTTL})" }
val isCleanupDisabled = serverConfig.backupTTL == 0
if (isCleanupDisabled) {
return
}
val automatedBackupDir = File(applicationDirs.automatedBackupRoot)
if (!automatedBackupDir.isDirectory) {
@@ -132,7 +132,7 @@ object ProtoBackupExport : ProtoBackupBase() {
val lastAccessTime = file.lastModified()
val isTTLReached =
System.currentTimeMillis() - lastAccessTime >= serverConfig.backupTTL.days.inWholeMilliseconds
System.currentTimeMillis() - lastAccessTime >= serverConfig.backupTTL.days.coerceAtLeast(1.days).inWholeMilliseconds
if (isTTLReached) {
file.delete()
}

View File

@@ -1,6 +1,8 @@
package suwayomi.tachidesk.manga.impl.update
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import it.sauronsoftware.cron4j.Task
import it.sauronsoftware.cron4j.TaskExecutionContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -24,11 +26,13 @@ import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.util.HAScheduler
import java.util.Date
import java.util.Timer
import java.util.TimerTask
@@ -51,41 +55,46 @@ class Updater : IUpdater {
private val lastAutomatedUpdateKey = "lastAutomatedUpdateKey"
private val preferences = Preferences.userNodeForPackage(Updater::class.java)
private val updateTimer = Timer()
private var currentUpdateTask: TimerTask? = null
private var currentUpdateTaskId = ""
init {
scheduleUpdateTask()
}
private fun autoUpdateTask() {
val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0)
preferences.putLong(lastAutomatedUpdateKey, System.currentTimeMillis())
if (status.value.running) {
logger.debug { "Global update is already in progress" }
return
}
logger.info { "Trigger global update (interval= ${serverConfig.globalUpdateInterval}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" }
addCategoriesToUpdateQueue(Category.getCategoryList(), true)
}
private fun scheduleUpdateTask() {
HAScheduler.deschedule(currentUpdateTaskId)
if (!serverConfig.automaticallyTriggerGlobalUpdate) {
return
}
val minInterval = 6.hours
val interval = serverConfig.globalUpdateInterval.hours
val updateInterval = interval.coerceAtLeast(minInterval).inWholeMilliseconds
val updateInterval = interval.coerceAtLeast(minInterval)
val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, System.currentTimeMillis())
val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0)
val initialDelay = updateInterval - (System.currentTimeMillis() - lastAutomatedUpdate) % updateInterval
currentUpdateTask?.cancel()
currentUpdateTask = object : TimerTask() {
override fun run() {
preferences.putLong(lastAutomatedUpdateKey, System.currentTimeMillis())
if (status.value.running) {
logger.debug { "Global update is already in progress, do not trigger global update" }
return
}
logger.info { "Trigger global update (interval= ${serverConfig.globalUpdateInterval}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" }
addCategoriesToUpdateQueue(Category.getCategoryList(), true)
}
// trigger update in case the server wasn't running on the scheduled time
val wasPreviousUpdateTriggered =
(System.currentTimeMillis() - lastAutomatedUpdate) < updateInterval.inWholeMilliseconds
if (!wasPreviousUpdateTriggered) {
autoUpdateTask()
}
updateTimer.scheduleAtFixedRate(currentUpdateTask, initialDelay, updateInterval)
HAScheduler.schedule(::autoUpdateTask, "* */${updateInterval.inWholeHours} * * *", "global-update")
}
private fun getOrCreateUpdateChannelFor(source: String): Channel<UpdateJob> {