Show failed migrations instead of waiting on them forever

This commit is contained in:
AntsyLich
2026-10-12 13:56:33 +06:00
parent a999e49f2f
commit 9dbb7148c5
6 changed files with 50 additions and 2 deletions
+1
View File
@@ -44,6 +44,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- Fixed the app closing when WebView's page process crashed ([@AntsyLich](https://github.com/AntsyLich)) - Fixed the app closing when WebView's page process crashed ([@AntsyLich](https://github.com/AntsyLich))
- Fixed installing or updating an extension sometimes cancelling the install of another one ([@AntsyLich](https://github.com/AntsyLich)) - Fixed installing or updating an extension sometimes cancelling the install of another one ([@AntsyLich](https://github.com/AntsyLich))
- Fixed updating from v0.20.x taking up to hours on libraries with many duplicate entries ([@AntsyLich](https://github.com/AntsyLich)) - Fixed updating from v0.20.x taking up to hours on libraries with many duplicate entries ([@AntsyLich](https://github.com/AntsyLich))
- Fixed the app staying on the updating screen for good when updating its data failed, it now shows what went wrong, and updating failing on leftover data of deleted entries ([@AntsyLich](https://github.com/AntsyLich))
## [v0.21.0] - 2026-10-12 ## [v0.21.0] - 2026-10-12
### Added ### Added
@@ -179,6 +179,9 @@ class MainActivity : BaseActivity() {
Migrator.awaitAndRelease() Migrator.awaitAndRelease()
isAppMigrating.value = false isAppMigrating.value = false
} }
// Uncaught on purpose: a failed database migration otherwise leaves the migrating screen up for good,
// while the crash screen tells what went wrong and lets the logs be shared
lifecycleScope.launch { databaseRepository.awaitOpen() }
// Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
if (!isTaskRoot) { if (!isTaskRoot) {
@@ -1,9 +1,11 @@
package mihon.core.migration package mihon.core.migration
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async import kotlinx.coroutines.async
import logcat.LogPriority
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
class MigrationJobFactory( class MigrationJobFactory(
@@ -20,7 +22,18 @@ class MigrationJobFactory(
} }
async { async {
val prev = acc.await() val prev = acc.await()
migration(migrationContext) || prev // A failure would cancel the rest of the chain and leave the app waiting on it for good
val ran = try {
migration(migrationContext)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) {
"Migration failed: { name = ${migration::class.simpleName}, version = ${migration.version} }"
}
false
}
ran || prev
} }
} else { } else {
logcat { logcat {
@@ -1,16 +1,29 @@
package mihon.data.database package mihon.data.database
import app.cash.sqldelight.db.QueryResult
import app.cash.sqldelight.db.SqlDriver
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.SingleIn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
import mihon.domain.database.repository.DatabaseRepository import mihon.domain.database.repository.DatabaseRepository
@Inject @Inject
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DatabaseRepositoryImpl : DatabaseRepository { class DatabaseRepositoryImpl(
// Lazy since the driver reports its migration state here
private val driver: Lazy<SqlDriver>,
) : DatabaseRepository {
override val isMigrating = MutableStateFlow(false) override val isMigrating = MutableStateFlow(false)
override suspend fun awaitOpen() {
withContext(Dispatchers.IO) {
driver.value.executeQuery(null, "SELECT 1", { QueryResult.Value(Unit) }, 0).await()
}
}
} }
@@ -4,6 +4,19 @@ import kotlin.String;
import kotlin.collections.List; import kotlin.collections.List;
import kotlinx.serialization.json.JsonObject; import kotlinx.serialization.json.JsonObject;
-- ---------------------------------------------------------------------------------------------------------
-- orphans
-- ---------------------------------------------------------------------------------------------------------
-- Rows pointing at an entry, chapter or category that no longer exists were written while foreign keys weren't
-- enforced. Nothing can reach them, and copied over they'd fail the foreign key check that ends the migration.
DELETE FROM chapters WHERE manga_id NOT IN (SELECT _id FROM mangas);
DELETE FROM history WHERE chapter_id NOT IN (SELECT _id FROM chapters);
DELETE FROM mangas_categories
WHERE manga_id NOT IN (SELECT _id FROM mangas) OR category_id NOT IN (SELECT _id FROM categories);
DELETE FROM manga_sync WHERE manga_id NOT IN (SELECT _id FROM mangas);
DELETE FROM excluded_scanlators WHERE manga_id NOT IN (SELECT _id FROM mangas);
-- --------------------------------------------------------------------------------------------------------- -- ---------------------------------------------------------------------------------------------------------
-- table - manga -- table - manga
-- --------------------------------------------------------------------------------------------------------- -- ---------------------------------------------------------------------------------------------------------
@@ -9,4 +9,9 @@ interface DatabaseRepository {
* app update. * app update.
*/ */
val isMigrating: StateFlow<Boolean> val isMigrating: StateFlow<Boolean>
/**
* Opens the database, migrating it if needed, and throws what made the migration fail.
*/
suspend fun awaitOpen()
} }