From 96a9a0536d57983df4718198a04881281aee5e4e Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 19 Aug 2026 06:00:18 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D1=83=D1=81=D1=82=D0=BE=D0=B9=D1=87?= =?UTF-8?q?=D0=B8=D0=B2=D0=BE=D0=B5=20=D1=81=D0=BA=D0=B0=D1=87=D0=B8=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=B7=20Jellyfin=20=E2=80=94?= =?UTF-8?q?=20OkHttp=20+=20retry=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20?= =?UTF-8?q?HttpURLConnection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Прод-инцидент (3 джоба failed подряд): HttpURLConnection рвал скачивание 7.2 ГБ на ~5-й минуте ('Can't download video from Jellyfin'), при этом curl с той же машины качал стабильно (HTTP/1.1 и HTTP/2). Сеть и Jellyfin чисты. - JellyfinInput: OkHttpClient (readTimeout=0, followRedirects), byteStream→Files.copy, retry до 3 попыток с паузой 5с на IOException, warn-лог каждой попытки - InputService: лог с полным stacktrace (реальная причина обрыва теперь видна) --- build.gradle.kts | 1 + .../mirror/worker/convert/InputService.kt | 1 + .../mirror/worker/convert/JellyfinInput.kt | 56 +++++++++++++------ 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 0c7bbdd..016f553 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0") implementation(platform("software.amazon.awssdk:bom:2.46.21")) implementation("software.amazon.awssdk:s3") + implementation("com.squareup.okhttp3:okhttp:4.12.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation(kotlin("test-junit5")) diff --git a/src/main/kotlin/pw/binom/mirror/worker/convert/InputService.kt b/src/main/kotlin/pw/binom/mirror/worker/convert/InputService.kt index 03f0017..981bd2c 100644 --- a/src/main/kotlin/pw/binom/mirror/worker/convert/InputService.kt +++ b/src/main/kotlin/pw/binom/mirror/worker/convert/InputService.kt @@ -28,6 +28,7 @@ class InputService( logger.info("Download cancelled: {}", source) throw e } catch (e: Throwable) { + logger.warn("Download failed: {}", e.message, e) throw IllegalStateException("Can't download video from $source", e) } logger.info("Downloaded success in {} ms", (System.nanoTime() - startTime) / 1_000_000) diff --git a/src/main/kotlin/pw/binom/mirror/worker/convert/JellyfinInput.kt b/src/main/kotlin/pw/binom/mirror/worker/convert/JellyfinInput.kt index ae04841..9004b7d 100644 --- a/src/main/kotlin/pw/binom/mirror/worker/convert/JellyfinInput.kt +++ b/src/main/kotlin/pw/binom/mirror/worker/convert/JellyfinInput.kt @@ -1,15 +1,18 @@ package pw.binom.mirror.worker.convert import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.stereotype.Component -import java.net.HttpURLConnection -import java.net.URL +import java.io.IOException import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardCopyOption +import java.util.concurrent.TimeUnit @Component class JellyfinInput( @@ -17,24 +20,46 @@ class JellyfinInput( ) : InputService.InputImplementation { private val logger: Logger = LoggerFactory.getLogger(JellyfinInput::class.java) + private val client: OkHttpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(0, TimeUnit.SECONDS) + .followRedirects(true) + .followSslRedirects(true) + .build() + } + override fun isSupport(source: InputSource): Boolean = source is InputSource.Jellyfin override suspend fun download(source: InputSource): Path = withContext(Dispatchers.IO) { source as InputSource.Jellyfin - val url = URL(source.streamUrl) - val connection = url.openConnection() as HttpURLConnection - connection.requestMethod = "GET" - connection.instanceFollowRedirects = true - connection.connectTimeout = 30_000 - connection.readTimeout = 120_000 - try { - if (connection.responseCode != HttpURLConnection.HTTP_OK) { - throw IllegalStateException("Invalid response code ${connection.responseCode}") + var lastError: IOException? = null + for (attempt in 1..3) { + try { + return@withContext downloadOnce(source) + } catch (e: IOException) { + lastError = e + logger.warn("Download attempt {}/3 failed: {}", attempt, e.message) + if (attempt < 3) { + delay(5_000) + } + } + } + throw lastError!! + } + + private fun downloadOnce(source: InputSource.Jellyfin): Path { + val request = Request.Builder().url(source.streamUrl).get().build() + val response = client.newCall(request).execute() + response.use { + if (!response.isSuccessful) { + throw IllegalStateException("Invalid response code ${response.code}") } val resultFile = localStorageService.genFile() - logger.info("Downloading file in to ${resultFile}...") - try { - connection.inputStream.use { input -> + logger.info("Downloading file in to {}...", resultFile) + val path = try { + val body = response.body ?: throw IllegalStateException("Empty response body") + body.byteStream().use { input -> Files.copy(input, resultFile, StandardCopyOption.REPLACE_EXISTING) } logger.info("File success downloaded!") @@ -43,8 +68,7 @@ class JellyfinInput( runCatching { Files.deleteIfExists(resultFile) } throw e } - } finally { - connection.disconnect() + return path } } }