Move blocking work off the main thread and fix threading races (#4068)

* Defer startup work until first use

* Move cover work off the main thread

* Make tracker calls main-safe and token refresh thread-safe

Kitsu, Shikimori and MangaBaka refresh their token under a lock like
MAL, so parallel calls no longer refresh it twice.

* Fix extension threading and make installs cancellable

The store service read response bodies on Main, which failed with
NetworkOnMainThreadException for http stores.

* Move reader work off the main thread and save progress in order

* Serialize the downloader and fix download threading

* Stop blocking on WorkManager status queries

Also removes NotificationReceiver's open chapter action: nothing sends
it, and its handler used runBlocking.

* Start migration jobs on the migrator's dispatcher

They started undispatched, so each ran on the caller's thread until its
first suspension.

* Run screen and view model work on fitting dispatchers

* Launch work that outlives a screen on the app scope

A launch on another dispatcher is dropped if the screen closes before it
starts, which dialogs that dismiss right away often do. Only the UI
follow-up stays on viewModelScope.

* Remove the coroutine helper extensions

They hid which dispatcher and scope a call used, and the GlobalScope
ones hid that it was global.
This commit is contained in:
AntsyLich
2026-10-10 17:08:49 +00:00
committed by GitHub
parent f9c5fa9011
commit c46317f099
131 changed files with 1612 additions and 1274 deletions
@@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.okio.decodeFromBufferedSource
@@ -30,7 +32,9 @@ class ExtensionStoreService(
private val json: Json,
private val protoBuf: ProtoBuf,
) {
suspend fun fetch(indexUrl: String): Result<ExtensionStore> {
suspend fun fetch(indexUrl: String): Result<ExtensionStore> = withContext(Dispatchers.IO) { fetchStore(indexUrl) }
private suspend fun fetchStore(indexUrl: String): Result<ExtensionStore> {
var updatedIndexUrl: String = indexUrl
return try {
val response = network.client.newCall(GET(updatedIndexUrl)).awaitSuccess()
@@ -56,7 +60,7 @@ class ExtensionStoreService(
}
if (networkStore is NetworkLegacyExtensionRepo && networkStore.indexV2 != null) {
return fetch(networkStore.indexV2)
return fetchStore(networkStore.indexV2)
}
networkStore.toExtensionStore(updatedIndexUrl)
@@ -72,8 +76,8 @@ class ExtensionStoreService(
}
}
suspend fun getExtensions(store: ExtensionStore): Result<List<Extension.Available>> {
return try {
suspend fun getExtensions(store: ExtensionStore): Result<List<Extension.Available>> = withContext(Dispatchers.IO) {
try {
val extensions = if (store.extensionListUrl != null) {
val response = network.client.newCall(GET(store.extensionListUrl!!)).awaitSuccess()
response.body.source().decompressIfGzipped().use { source ->
@@ -9,6 +9,8 @@ import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.domain.release.model.Release
@@ -23,11 +25,13 @@ class ReleaseServiceImpl(
) : ReleaseService {
override suspend fun latest(arguments: GetApplicationRelease.Arguments): Release? {
val release = with(json) {
networkService.client
.newCall(GET("https://api.github.com/repos/${arguments.repository}/releases/latest"))
.awaitSuccess()
.parseAs<GithubRelease>()
val release = withContext(Dispatchers.IO) {
with(json) {
networkService.client
.newCall(GET("https://api.github.com/repos/${arguments.repository}/releases/latest"))
.awaitSuccess()
.parseAs<GithubRelease>()
}
}
val downloadLink = getDownloadLink(release = release, isFoss = arguments.isFoss) ?: return null
@@ -5,8 +5,9 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mihon.domain.manga.model.toDomainManga
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.manga.interactor.NetworkToLocalManga
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.source.repository.SourcePagingSource
@@ -54,7 +55,7 @@ abstract class BaseSourcePagingSource(
return try {
val source = source()
val mangasPage = withIOContext {
val mangasPage = withContext(Dispatchers.IO) {
requestNextPage(source, page.toInt())
.takeIf { it.mangas.isNotEmpty() }
?: throw NoResultsException()