mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-08-04 10:36:56 +00:00
fix(simkl): pace requests after completion
This commit is contained in:
parent
c85831df26
commit
d8ffd18c4d
2 changed files with 65 additions and 18 deletions
|
|
@ -95,17 +95,18 @@ internal class SimklApiClient(
|
|||
}
|
||||
var syncWriteLockRetried = false
|
||||
for (attempt in 0 until maxAttempts) {
|
||||
awaitRateLimit(request.method)
|
||||
val response = try {
|
||||
engine.execute(
|
||||
method = request.method.name,
|
||||
url = buildSimklApiUrl(request.path, request.query),
|
||||
headers = simklRequestHeaders(
|
||||
accessToken = token,
|
||||
contentTypeJson = request.method == SimklHttpMethod.POST,
|
||||
),
|
||||
body = request.body,
|
||||
)
|
||||
executeRateLimited(request.method) {
|
||||
engine.execute(
|
||||
method = request.method.name,
|
||||
url = buildSimklApiUrl(request.path, request.query),
|
||||
headers = simklRequestHeaders(
|
||||
accessToken = token,
|
||||
contentTypeJson = request.method == SimklHttpMethod.POST,
|
||||
),
|
||||
body = request.body,
|
||||
)
|
||||
}
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
|
|
@ -165,17 +166,36 @@ internal class SimklApiClient(
|
|||
error("Simkl request loop completed without a response")
|
||||
}
|
||||
|
||||
private suspend fun <T> executeRateLimited(
|
||||
method: SimklHttpMethod,
|
||||
block: suspend () -> T,
|
||||
): T {
|
||||
awaitRateLimit(method)
|
||||
return try {
|
||||
block()
|
||||
} finally {
|
||||
recordRateLimitCompletion(method)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun awaitRateLimit(method: SimklHttpMethod) {
|
||||
val now = nowEpochMs()
|
||||
val scheduledAt = when (method) {
|
||||
SimklHttpMethod.GET -> max(now, nextGetAtEpochMs)
|
||||
SimklHttpMethod.POST, SimklHttpMethod.DELETE -> max(now, nextPostAtEpochMs)
|
||||
SimklHttpMethod.GET -> nextGetAtEpochMs
|
||||
SimklHttpMethod.POST, SimklHttpMethod.DELETE -> nextPostAtEpochMs
|
||||
}
|
||||
if (scheduledAt > now) sleep(scheduledAt - now)
|
||||
val requestAt = max(scheduledAt, nowEpochMs())
|
||||
}
|
||||
|
||||
private fun recordRateLimitCompletion(method: SimklHttpMethod) {
|
||||
val completedAt = nowEpochMs()
|
||||
when (method) {
|
||||
SimklHttpMethod.GET -> nextGetAtEpochMs = requestAt + GET_INTERVAL_MS
|
||||
SimklHttpMethod.POST, SimklHttpMethod.DELETE -> nextPostAtEpochMs = requestAt + POST_INTERVAL_MS
|
||||
SimklHttpMethod.GET -> {
|
||||
nextGetAtEpochMs = max(nextGetAtEpochMs, completedAt + GET_INTERVAL_MS)
|
||||
}
|
||||
SimklHttpMethod.POST, SimklHttpMethod.DELETE -> {
|
||||
nextPostAtEpochMs = max(nextPostAtEpochMs, completedAt + POST_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -173,6 +173,26 @@ class SimklApiClientTest {
|
|||
assertEquals(listOf(0L, 100L, 100L, 1_100L), engine.requests.map { it.atEpochMs })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `post cooldown starts after the previous response completes`() = runBlocking {
|
||||
val engine = RecordingEngine(response(200), response(200))
|
||||
val harness = TestHarness(engine, responseDurationMs = 400L)
|
||||
|
||||
harness.client.execute(
|
||||
SimklApiRequest(
|
||||
method = SimklHttpMethod.POST,
|
||||
path = "/oauth/token",
|
||||
body = "{}",
|
||||
requiresAuthentication = false,
|
||||
retryPolicy = SimklRetryPolicy.NEVER,
|
||||
),
|
||||
)
|
||||
harness.client.execute(SimklApiRequest(SimklHttpMethod.POST, "/users/settings"))
|
||||
|
||||
assertEquals(listOf(1_000L), harness.sleeps)
|
||||
assertEquals(listOf(0L, 1_400L), engine.requests.map { it.atEpochMs })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transient responses retry sequentially and deterministic errors do not`() = runBlocking {
|
||||
val transientEngine = RecordingEngine(response(503), response(502), response(200))
|
||||
|
|
@ -290,12 +310,18 @@ class SimklApiClientTest {
|
|||
assertFalse(harness.wasUnauthorized)
|
||||
}
|
||||
|
||||
private class TestHarness(engine: RecordingEngine) {
|
||||
private class TestHarness(
|
||||
engine: RecordingEngine,
|
||||
responseDurationMs: Long = 0L,
|
||||
) {
|
||||
var now = 0L
|
||||
val sleeps = mutableListOf<Long>()
|
||||
var wasUnauthorized = false
|
||||
val client = SimklApiClient(
|
||||
engine = engine.also { recording -> recording.now = { now } },
|
||||
engine = engine.also { recording ->
|
||||
recording.now = { now }
|
||||
recording.onResponse = { now += responseDurationMs }
|
||||
},
|
||||
accessToken = { "token" },
|
||||
onUnauthorized = { wasUnauthorized = true },
|
||||
nowEpochMs = { now },
|
||||
|
|
@ -311,6 +337,7 @@ class SimklApiClientTest {
|
|||
private val queuedResponses = responses.toMutableList()
|
||||
val requests = mutableListOf<RecordedRequest>()
|
||||
var now: () -> Long = { 0L }
|
||||
var onResponse: () -> Unit = {}
|
||||
|
||||
override suspend fun execute(
|
||||
method: String,
|
||||
|
|
@ -319,7 +346,7 @@ class SimklApiClientTest {
|
|||
body: String,
|
||||
): RawHttpResponse {
|
||||
requests += RecordedRequest(method, url, headers, body, now())
|
||||
return queuedResponses.removeAt(0)
|
||||
return queuedResponses.removeAt(0).also { onResponse() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue