fix: устойчивое скачивание из Jellyfin — OkHttp + retry вместо HttpURLConnection
Build Media Mirror Worker / Build and publish (release) Successful in 1m25s
Build Media Mirror Worker / Build and publish (release) Successful in 1m25s
Прод-инцидент (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 (реальная причина обрыва теперь видна)
This commit is contained in:
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user