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("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
|
||||||
implementation(platform("software.amazon.awssdk:bom:2.46.21"))
|
implementation(platform("software.amazon.awssdk:bom:2.46.21"))
|
||||||
implementation("software.amazon.awssdk:s3")
|
implementation("software.amazon.awssdk:s3")
|
||||||
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
testImplementation(kotlin("test-junit5"))
|
testImplementation(kotlin("test-junit5"))
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class InputService(
|
|||||||
logger.info("Download cancelled: {}", source)
|
logger.info("Download cancelled: {}", source)
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
logger.warn("Download failed: {}", e.message, e)
|
||||||
throw IllegalStateException("Can't download video from $source", e)
|
throw IllegalStateException("Can't download video from $source", e)
|
||||||
}
|
}
|
||||||
logger.info("Downloaded success in {} ms", (System.nanoTime() - startTime) / 1_000_000)
|
logger.info("Downloaded success in {} ms", (System.nanoTime() - startTime) / 1_000_000)
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
package pw.binom.mirror.worker.convert
|
package pw.binom.mirror.worker.convert
|
||||||
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.net.HttpURLConnection
|
import java.io.IOException
|
||||||
import java.net.URL
|
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.nio.file.StandardCopyOption
|
import java.nio.file.StandardCopyOption
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
class JellyfinInput(
|
class JellyfinInput(
|
||||||
@@ -17,24 +20,46 @@ class JellyfinInput(
|
|||||||
) : InputService.InputImplementation {
|
) : InputService.InputImplementation {
|
||||||
private val logger: Logger = LoggerFactory.getLogger(JellyfinInput::class.java)
|
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 fun isSupport(source: InputSource): Boolean = source is InputSource.Jellyfin
|
||||||
|
|
||||||
override suspend fun download(source: InputSource): Path = withContext(Dispatchers.IO) {
|
override suspend fun download(source: InputSource): Path = withContext(Dispatchers.IO) {
|
||||||
source as InputSource.Jellyfin
|
source as InputSource.Jellyfin
|
||||||
val url = URL(source.streamUrl)
|
var lastError: IOException? = null
|
||||||
val connection = url.openConnection() as HttpURLConnection
|
for (attempt in 1..3) {
|
||||||
connection.requestMethod = "GET"
|
try {
|
||||||
connection.instanceFollowRedirects = true
|
return@withContext downloadOnce(source)
|
||||||
connection.connectTimeout = 30_000
|
} catch (e: IOException) {
|
||||||
connection.readTimeout = 120_000
|
lastError = e
|
||||||
try {
|
logger.warn("Download attempt {}/3 failed: {}", attempt, e.message)
|
||||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
if (attempt < 3) {
|
||||||
throw IllegalStateException("Invalid response code ${connection.responseCode}")
|
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()
|
val resultFile = localStorageService.genFile()
|
||||||
logger.info("Downloading file in to ${resultFile}...")
|
logger.info("Downloading file in to {}...", resultFile)
|
||||||
try {
|
val path = try {
|
||||||
connection.inputStream.use { input ->
|
val body = response.body ?: throw IllegalStateException("Empty response body")
|
||||||
|
body.byteStream().use { input ->
|
||||||
Files.copy(input, resultFile, StandardCopyOption.REPLACE_EXISTING)
|
Files.copy(input, resultFile, StandardCopyOption.REPLACE_EXISTING)
|
||||||
}
|
}
|
||||||
logger.info("File success downloaded!")
|
logger.info("File success downloaded!")
|
||||||
@@ -43,8 +68,7 @@ class JellyfinInput(
|
|||||||
runCatching { Files.deleteIfExists(resultFile) }
|
runCatching { Files.deleteIfExists(resultFile) }
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
} finally {
|
return path
|
||||||
connection.disconnect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user