Mirror: медленный list(500) не роняет каталог и не дёргается в цикле
- отдельный defaultMirrorHttpClient (connect 15с / request 90с) + 1 повтор на list(): тяжёлый GET /api/mirror?limit=N на сервере дольше общего 30с-таймаута и подёргивается (503); - каталог: зеркала грузятся в отдельном try — сбой/таймаут не чистит уже загруженный каталог, просто временно без бейджей; - детали: убрали тяжёлый list(limit=500) каждые 3 с (progress зеркала на сервере не живой, 0 до конца) — в цикле опрашивается только дешёвый mirrorByItem.
This commit is contained in:
@@ -52,6 +52,7 @@ import pw.binom.viewmate.core.media.MirrorJob
|
|||||||
import pw.binom.viewmate.core.protocol.DownloadPhase
|
import pw.binom.viewmate.core.protocol.DownloadPhase
|
||||||
import pw.binom.viewmate.phone.PhoneApp
|
import pw.binom.viewmate.phone.PhoneApp
|
||||||
import pw.binom.viewmate.phone.WatchProgress
|
import pw.binom.viewmate.phone.WatchProgress
|
||||||
|
import pw.binom.viewmate.phone.log
|
||||||
|
|
||||||
/** Вкладки каталога: Фильмы / Сериалы / Папки. */
|
/** Вкладки каталога: Фильмы / Сериалы / Папки. */
|
||||||
enum class CatalogTab { MOVIES, SERIES, FOLDERS }
|
enum class CatalogTab { MOVIES, SERIES, FOLDERS }
|
||||||
@@ -205,7 +206,6 @@ class CatalogState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
items = loaded
|
items = loaded
|
||||||
mirrors = app.mirror.list(limit = 500).associateBy { it.itemId.orEmpty() }
|
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -214,6 +214,15 @@ class CatalogState(
|
|||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
// Зеркала — отдельный «мягкий» запрос: сбой/таймаут mirror.list не чистит
|
||||||
|
// уже загруженный каталог, просто временно без бейджей.
|
||||||
|
try {
|
||||||
|
mirrors = app.mirror.list(limit = 500).associateBy { it.itemId.orEmpty() }
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log("catalog", "зеркала: не удалось (${e.message}) — каталог без бейджей")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,16 +78,16 @@ fun DetailsScreen(
|
|||||||
val glassesDownloadedIds by app.server.hub.downloadedItemIds.collectAsState()
|
val glassesDownloadedIds by app.server.hub.downloadedItemIds.collectAsState()
|
||||||
val glassesMediaDir by app.server.hub.mediaDir.collectAsState()
|
val glassesMediaDir by app.server.hub.mediaDir.collectAsState()
|
||||||
|
|
||||||
// статус зеркала + дорожки; пока конвертируется — опрашиваем
|
// статус зеркала + дорожки; пока конвертируется — опрашиваем только дешёвый byItem.
|
||||||
|
// Тяжёлый list(limit=500) НЕ дёргаем каждые 3 с: progress зеркала на сервере не живой
|
||||||
|
// (0 до конца), а тяжёлый запрос с большим лимитом — главный источник таймаутов.
|
||||||
LaunchedEffect(item.Id, refreshKey) {
|
LaunchedEffect(item.Id, refreshKey) {
|
||||||
loading = true
|
loading = true
|
||||||
mirrorStatus = runCatching { app.mirror.mirrorByItem(item.Id) }.getOrNull()
|
mirrorStatus = runCatching { app.mirror.mirrorByItem(item.Id) }.getOrNull()
|
||||||
mirrorJob = runCatching { app.mirror.list(limit = 500).firstOrNull { it.itemId == item.Id } }.getOrNull()
|
|
||||||
loading = false
|
loading = false
|
||||||
while (mirrorStatus?.status == "processing" || mirrorStatus?.status == "queued") {
|
while (mirrorStatus?.status == "processing" || mirrorStatus?.status == "queued") {
|
||||||
delay(3_000)
|
delay(3_000)
|
||||||
mirrorStatus = runCatching { app.mirror.mirrorByItem(item.Id) }.getOrNull()
|
mirrorStatus = runCatching { app.mirror.mirrorByItem(item.Id) }.getOrNull()
|
||||||
mirrorJob = runCatching { app.mirror.list(limit = 500).firstOrNull { it.itemId == item.Id } }.getOrNull()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,35 @@ fun defaultHttpClient(): HttpClient = HttpClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP-клиент для media-mirror: тяжёлые опросы (mirror.list с большим лимитом)
|
||||||
|
* на сервере считаются заметно дольше, чем обычные запросы, поэтому таймауты
|
||||||
|
* шире [defaultHttpClient] — иначе живой, но медленный ответ упирается в таймаут.
|
||||||
|
*/
|
||||||
|
fun defaultMirrorHttpClient(): HttpClient = HttpClient {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(
|
||||||
|
Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
isLenient = true
|
||||||
|
explicitNulls = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
install(HttpTimeout) {
|
||||||
|
connectTimeoutMillis = 15_000
|
||||||
|
requestTimeoutMillis = 90_000
|
||||||
|
}
|
||||||
|
install(Logging) {
|
||||||
|
logger = object : Logger {
|
||||||
|
override fun log(message: String) {
|
||||||
|
if (HttpClientDebugLogging.enabled) println("[view-mate] $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
level = LogLevel.INFO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Флаг debug-логирования HTTP (по умолчанию включён). */
|
/** Флаг debug-логирования HTTP (по умолчанию включён). */
|
||||||
object HttpClientDebugLogging {
|
object HttpClientDebugLogging {
|
||||||
var enabled: Boolean = true
|
var enabled: Boolean = true
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package pw.binom.viewmate.core.media
|
|||||||
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import io.ktor.client.request.delete
|
import io.ktor.client.request.delete
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.request.header
|
import io.ktor.client.request.header
|
||||||
@@ -19,7 +21,7 @@ import io.ktor.http.contentType
|
|||||||
class MirrorClient(
|
class MirrorClient(
|
||||||
private val baseUrl: String, // https://mirror.binom.pw
|
private val baseUrl: String, // https://mirror.binom.pw
|
||||||
private val apiKey: String, // X-API-Key
|
private val apiKey: String, // X-API-Key
|
||||||
private val http: HttpClient = defaultHttpClient(),
|
private val http: HttpClient = defaultMirrorHttpClient(),
|
||||||
) {
|
) {
|
||||||
/** Заказ конвертации элемента. 201 — создан, 200 — уже есть (дубль). */
|
/** Заказ конвертации элемента. 201 — создан, 200 — уже есть (дубль). */
|
||||||
suspend fun createMirror(itemId: String): MirrorJob {
|
suspend fun createMirror(itemId: String): MirrorJob {
|
||||||
@@ -42,15 +44,26 @@ class MirrorClient(
|
|||||||
return response.body()
|
return response.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Список зеркал: статус (null — все) + лимит. */
|
/** Тяжёлый запрос (большой limit, медленный сервер): повторяем при сбое. Статус (null — все) + лимит. */
|
||||||
suspend fun list(status: String? = null, limit: Int = 50): List<MirrorJob> {
|
suspend fun list(status: String? = null, limit: Int = 50): List<MirrorJob> {
|
||||||
val response = http.get("$baseUrl/api/mirror") {
|
var last: Exception? = null
|
||||||
header("X-API-Key", apiKey)
|
repeat(LIST_ATTEMPTS) { attempt ->
|
||||||
parameter("limit", limit)
|
try {
|
||||||
if (status != null) parameter("status", status)
|
val response = http.get("$baseUrl/api/mirror") {
|
||||||
|
header("X-API-Key", apiKey)
|
||||||
|
parameter("limit", limit)
|
||||||
|
if (status != null) parameter("status", status)
|
||||||
|
}
|
||||||
|
response.ensureSuccess("mirror.list")
|
||||||
|
return response.body()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
last = e
|
||||||
|
if (attempt < LIST_ATTEMPTS - 1) delay(LIST_RETRY_DELAY_MS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
response.ensureSuccess("mirror.list")
|
throw ApiClientException("mirror.list не удался: ${last?.message}", cause = last)
|
||||||
return response.body()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Удаление зеркала (204). */
|
/** Удаление зеркала (204). */
|
||||||
@@ -69,4 +82,11 @@ class MirrorClient(
|
|||||||
response.ensureSuccess("mirror.scan")
|
response.ensureSuccess("mirror.scan")
|
||||||
return response.body()
|
return response.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Сколько раз повторяем [list] при сбое (один повтор на подёргивание сервера). */
|
||||||
|
private const val LIST_ATTEMPTS = 2
|
||||||
|
|
||||||
|
private const val LIST_RETRY_DELAY_MS = 1_500L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user