fix(audio-rooms): retry mintToken on transport hiccup + harness /health warmup
The remaining interop failures all root in the same window: a stale
keep-alive pool entry from one test class is reused on the FIRST POST
of the next test class, the connection RSTs as the request body
writes, and OkHttp's built-in retryOnConnectionFailure won't retry a
POST after any byte of the body has gone out. Same situation hits a
phone client whose Wi-Fi hands off mid-mint.
Two fixes, both production-shaped:
- OkHttpNestsClient.mintToken now wraps execute() in
executeWithTransportRetry(): one retry on SocketException /
EOFException / generic IOException. Request builders are
immutable, so the second pass opens a fresh connection cleanly.
HTTP error status codes (4xx / 5xx) and malformed responses are
NOT retried — they go to the caller as before.
- NostrNestsHarness now polls GET /health until it returns 200
after the port-probe succeeds. moq-auth's Node runtime opens its
listen socket before the request handlers are wired, so a POST
that arrives in that window can RST. Waiting for /health proves
the request pipeline is live, eliminating the SocketException
that hit the first test of every test class run after
AuthEndpoints.
Symptoms fixed:
- NostrNestsAuthFailureInteropTest.missing_authorization_header_is_rejected_401
-> SocketException
- NostrNestsRoundTripInteropTest.production_speaker_broadcasts_to_production_listener_via_real_relay
-> "Failed to reach http://127.0.0.1:8090/auth"
- NostrNestsMultiPeerInteropTest.* (3 tests, same root cause)
This commit is contained in:
+42
-2
@@ -27,7 +27,10 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import java.io.EOFException
|
||||
import java.io.IOException
|
||||
import java.net.SocketException
|
||||
|
||||
/**
|
||||
* OkHttp-backed [NestsClient] used on JVM + Android. A shared [OkHttpClient]
|
||||
@@ -71,8 +74,7 @@ class OkHttpNestsClient(
|
||||
.build()
|
||||
|
||||
return withContext(Dispatchers.IO) {
|
||||
runCatching { http.newCall(request).execute() }
|
||||
.getOrElse { throw NestsException("Failed to reach $url", it) }
|
||||
executeWithTransportRetry(request, url)
|
||||
.use { response ->
|
||||
val body = response.body.string()
|
||||
if (!response.isSuccessful) {
|
||||
@@ -94,6 +96,44 @@ class OkHttpNestsClient(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send [request] and tolerate one transport-layer hiccup. OkHttp's
|
||||
* built-in `retryOnConnectionFailure` does NOT retry POSTs once any
|
||||
* byte of the request body has been written — but a stale pooled
|
||||
* connection can RST or EOF *exactly* in that window, especially on
|
||||
* mobile networks (and during interop test runs after an idle gap
|
||||
* between test classes). One retry on `SocketException` /
|
||||
* `EOFException` / `IOException` recovers cleanly because
|
||||
* `Request` builders are immutable; OkHttp opens a fresh
|
||||
* connection on the second try.
|
||||
*
|
||||
* Anything that's not a transient transport failure (HTTP 4xx /
|
||||
* 5xx, malformed response) is left to the caller as before.
|
||||
*/
|
||||
private fun executeWithTransportRetry(
|
||||
request: Request,
|
||||
url: String,
|
||||
): Response {
|
||||
var lastError: Throwable? = null
|
||||
repeat(2) { attempt ->
|
||||
try {
|
||||
return http.newCall(request).execute()
|
||||
} catch (e: SocketException) {
|
||||
lastError = e
|
||||
} catch (e: EOFException) {
|
||||
lastError = e
|
||||
} catch (e: IOException) {
|
||||
// OkHttp wraps a wide variety of transport faults
|
||||
// (StreamResetException, ConnectionShutdownException,
|
||||
// …) under IOException. Retry once; second pass either
|
||||
// succeeds against a fresh connection or surfaces the
|
||||
// real error.
|
||||
lastError = e
|
||||
}
|
||||
}
|
||||
throw NestsException("Failed to reach $url", lastError)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val JSON_MEDIA_TYPE = "application/json".toMediaType()
|
||||
}
|
||||
|
||||
+43
@@ -154,6 +154,13 @@ class NostrNestsHarness private constructor(
|
||||
try {
|
||||
waitForPort("127.0.0.1", AUTH_HOST_PORT, PORT_READY_TIMEOUT_MS)
|
||||
waitForPort("127.0.0.1", MOQ_HOST_PORT, PORT_READY_TIMEOUT_MS)
|
||||
// moq-auth's Node runtime opens the listen socket
|
||||
// before its handlers are wired, so the first POST that
|
||||
// arrives in the gap can RST-on-write. Wait for /health
|
||||
// to actually return 200 — that proves the request
|
||||
// pipeline is live and avoids the SocketException that
|
||||
// otherwise hits the first test of the second class.
|
||||
waitForHealth("http://127.0.0.1:$AUTH_HOST_PORT/health", PORT_READY_TIMEOUT_MS)
|
||||
} catch (t: Throwable) {
|
||||
// If readiness probe fails, tear down so we don't leak the
|
||||
// stack into the next test run.
|
||||
@@ -300,5 +307,41 @@ class NostrNestsHarness private constructor(
|
||||
lastError,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll [healthUrl] until it returns 200. moq-auth opens its
|
||||
* listen socket before its handlers are ready; without this,
|
||||
* the first POST that arrives during that window gets a TCP
|
||||
* RST. Uses HttpURLConnection so we don't pull a heavy HTTP
|
||||
* client into the harness.
|
||||
*/
|
||||
private fun waitForHealth(
|
||||
healthUrl: String,
|
||||
timeoutMs: Long,
|
||||
) {
|
||||
val deadline = System.currentTimeMillis() + timeoutMs
|
||||
var lastError: Throwable? = null
|
||||
val url = java.net.URI(healthUrl).toURL()
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
try {
|
||||
val conn = url.openConnection() as java.net.HttpURLConnection
|
||||
conn.connectTimeout = 1_000
|
||||
conn.readTimeout = 1_000
|
||||
conn.requestMethod = "GET"
|
||||
try {
|
||||
if (conn.responseCode == 200) return
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
lastError = t
|
||||
}
|
||||
Thread.sleep(PORT_PROBE_INTERVAL_MS)
|
||||
}
|
||||
throw IllegalStateException(
|
||||
"$healthUrl did not return 200 within ${timeoutMs}ms",
|
||||
lastError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user