Skip to content

Commit b38db9f

Browse files
committed
fix: expire DeArrow thumbnail fallback quickly
1 parent 4381324 commit b38db9f

3 files changed

Lines changed: 32 additions & 10 deletions

File tree

‎server-http/src/main/kotlin/dev/typetype/server/routes/DeArrowRoutes.kt‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ fun Route.deArrowRoutes(service: DeArrowService) {
3131
val videoId = call.request.queryParameters["videoId"].orEmpty()
3232
val timestamp = call.request.queryParameters["time"]?.toDoubleOrNull()
3333
?: return@get call.respond(HttpStatusCode.BadRequest, ErrorResponse("Invalid time"))
34-
val bytes = service.thumbnail(videoId, timestamp)
34+
val thumbnail = service.thumbnail(videoId, timestamp)
3535
?: return@get call.respond(HttpStatusCode.NotFound, ErrorResponse("Thumbnail not found"))
36-
call.response.headers.append(HttpHeaders.CacheControl, "public, max-age=604800")
37-
call.respondBytes(bytes, deArrowThumbnailContentType(bytes))
36+
val maxAge = if (thumbnail.fallback) 900 else 604800
37+
call.response.headers.append(HttpHeaders.CacheControl, "public, max-age=$maxAge")
38+
call.respondBytes(thumbnail.bytes, deArrowThumbnailContentType(thumbnail.bytes))
3839
}
3940
}
4041

‎server-services/src/main/kotlin/dev/typetype/server/services/DeArrowService.kt‎

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import java.util.Base64
1616

1717
class DeArrowUnavailableException : RuntimeException("DeArrow is temporarily unavailable")
1818

19+
data class DeArrowThumbnail(
20+
val bytes: ByteArray,
21+
val fallback: Boolean,
22+
)
23+
1924
class DeArrowService(
2025
private val cache: CacheService,
2126
private val client: DeArrowRemote = DeArrowClient(),
@@ -35,17 +40,27 @@ class DeArrowService(
3540
return item
3641
}
3742

38-
suspend fun thumbnail(videoId: String, timestamp: Double): ByteArray? {
43+
suspend fun thumbnail(videoId: String, timestamp: Double): DeArrowThumbnail? {
3944
if (!isValidVideoId(videoId) || !timestamp.isFinite() || timestamp < 0.0) return null
4045
val normalizedTime = "%.3f".format(java.util.Locale.ROOT, timestamp)
4146
val key = "dearrow:thumbnail:$videoId:$normalizedTime"
42-
cache.get(key)?.let { return runCatching { Base64.getDecoder().decode(it) }.getOrNull() }
47+
cache.get(key)?.let { return decodeThumbnail(it) }
4348
val generated = client.thumbnail(videoId, timestamp)?.takeIf { it.size <= MAX_THUMBNAIL_BYTES }
4449
val fallback = generated ?: client.fallbackThumbnail(videoId)?.takeIf { it.size <= MAX_THUMBNAIL_BYTES }
4550
val bytes = fallback ?: return null
46-
val ttl = if (generated != null) THUMBNAIL_TTL_SECONDS else FALLBACK_THUMBNAIL_TTL_SECONDS
47-
cache.set(key, Base64.getEncoder().encodeToString(bytes), ttl)
48-
return bytes
51+
val isFallback = generated == null
52+
val ttl = if (isFallback) FALLBACK_THUMBNAIL_TTL_SECONDS else THUMBNAIL_TTL_SECONDS
53+
val prefix = if (isFallback) FALLBACK_CACHE_PREFIX else GENERATED_CACHE_PREFIX
54+
cache.set(key, prefix + Base64.getEncoder().encodeToString(bytes), ttl)
55+
return DeArrowThumbnail(bytes, isFallback)
56+
}
57+
58+
private fun decodeThumbnail(value: String): DeArrowThumbnail? {
59+
val fallback = value.startsWith(FALLBACK_CACHE_PREFIX)
60+
val payload = value.removePrefix(FALLBACK_CACHE_PREFIX).removePrefix(GENERATED_CACHE_PREFIX)
61+
return runCatching {
62+
DeArrowThumbnail(Base64.getDecoder().decode(payload), fallback)
63+
}.getOrNull()
4964
}
5065

5166
private fun parse(videoId: String, raw: String): DeArrowItem {
@@ -123,6 +138,8 @@ class DeArrowService(
123138
private const val THUMBNAIL_TTL_SECONDS = 604_800L
124139
private const val FALLBACK_THUMBNAIL_TTL_SECONDS = 900L
125140
private const val MAX_THUMBNAIL_BYTES = 2 * 1024 * 1024
141+
private const val FALLBACK_CACHE_PREFIX = "fallback:"
142+
private const val GENERATED_CACHE_PREFIX = "generated:"
126143
private val VIDEO_ID_REGEX = Regex("^[A-Za-z0-9_-]{11}$")
127144
private val TITLE_MARKER_REGEX = Regex(">(?=\\S)")
128145
}

‎server-services/src/test/kotlin/dev/typetype/server/DeArrowServiceTest.kt‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,12 @@ class DeArrowServiceTest {
5959
val remote = FakeDeArrowRemote().apply { thumbnailResult = null }
6060
val service = DeArrowService(cache, remote)
6161

62-
assertArrayEquals(FakeDeArrowRemote.FALLBACK_THUMBNAIL, service.thumbnail("stZ3ZoR_8eg", 12.5))
63-
assertArrayEquals(FakeDeArrowRemote.FALLBACK_THUMBNAIL, service.thumbnail("stZ3ZoR_8eg", 12.5))
62+
val first = service.thumbnail("stZ3ZoR_8eg", 12.5)
63+
val second = service.thumbnail("stZ3ZoR_8eg", 12.5)
64+
assertArrayEquals(FakeDeArrowRemote.FALLBACK_THUMBNAIL, first?.bytes)
65+
assertEquals(true, first?.fallback)
66+
assertArrayEquals(FakeDeArrowRemote.FALLBACK_THUMBNAIL, second?.bytes)
67+
assertEquals(true, second?.fallback)
6468
assertEquals(1, remote.thumbnailCalls)
6569
assertEquals(1, remote.fallbackThumbnailCalls)
6670
}

0 commit comments

Comments
 (0)