Moves OKHttp relay implementations to use a Channel in order to guarantee incoming message order.
This commit is contained in:
+36
-4
@@ -24,6 +24,13 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
|||||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket.Companion.exceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -33,7 +40,6 @@ class OkHttpWebSocket(
|
|||||||
val httpClient: (url: NormalizedRelayUrl) -> OkHttpClient,
|
val httpClient: (url: NormalizedRelayUrl) -> OkHttpClient,
|
||||||
val out: WebSocketListener,
|
val out: WebSocketListener,
|
||||||
) : WebSocket {
|
) : WebSocket {
|
||||||
private val listener = OkHttpWebsocketListener()
|
|
||||||
private var usingOkHttp: OkHttpClient? = null
|
private var usingOkHttp: OkHttpClient? = null
|
||||||
private var socket: okhttp3.WebSocket? = null
|
private var socket: okhttp3.WebSocket? = null
|
||||||
|
|
||||||
@@ -64,10 +70,21 @@ class OkHttpWebSocket(
|
|||||||
|
|
||||||
override fun connect() {
|
override fun connect() {
|
||||||
usingOkHttp = httpClient(url)
|
usingOkHttp = httpClient(url)
|
||||||
socket = usingOkHttp?.newWebSocket(buildRequest(), listener)
|
socket = usingOkHttp?.newWebSocket(buildRequest(), OkHttpWebsocketListener(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class OkHttpWebsocketListener : okhttp3.WebSocketListener() {
|
inner class OkHttpWebsocketListener(
|
||||||
|
val out: WebSocketListener,
|
||||||
|
) : okhttp3.WebSocketListener() {
|
||||||
|
val scope = CoroutineScope(Dispatchers.Default + exceptionHandler)
|
||||||
|
val incomingMessages: Channel<String> = Channel(Channel.UNLIMITED)
|
||||||
|
val job = // Launch a coroutine to process messages from the channel.
|
||||||
|
scope.launch {
|
||||||
|
for (message in incomingMessages) {
|
||||||
|
out.onMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOpen(
|
override fun onOpen(
|
||||||
webSocket: okhttp3.WebSocket,
|
webSocket: okhttp3.WebSocket,
|
||||||
response: Response,
|
response: Response,
|
||||||
@@ -79,7 +96,12 @@ class OkHttpWebSocket(
|
|||||||
override fun onMessage(
|
override fun onMessage(
|
||||||
webSocket: okhttp3.WebSocket,
|
webSocket: okhttp3.WebSocket,
|
||||||
text: String,
|
text: String,
|
||||||
) = out.onMessage(text)
|
) {
|
||||||
|
// Asynchronously send the received message to the channel.
|
||||||
|
// `trySendBlocking` is used here for simplicity within the callback,
|
||||||
|
// but it's important to understand potential thread blocking if the buffer is full.
|
||||||
|
incomingMessages.trySendBlocking(text)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onClosing(
|
override fun onClosing(
|
||||||
webSocket: okhttp3.WebSocket,
|
webSocket: okhttp3.WebSocket,
|
||||||
@@ -92,6 +114,11 @@ class OkHttpWebSocket(
|
|||||||
code: Int,
|
code: Int,
|
||||||
reason: String,
|
reason: String,
|
||||||
) {
|
) {
|
||||||
|
// Close the channel on failure, and propagate the error.
|
||||||
|
incomingMessages.close()
|
||||||
|
job.cancel()
|
||||||
|
scope.cancel()
|
||||||
|
|
||||||
socket = null
|
socket = null
|
||||||
out.onClosed(code, reason)
|
out.onClosed(code, reason)
|
||||||
}
|
}
|
||||||
@@ -101,6 +128,11 @@ class OkHttpWebSocket(
|
|||||||
t: Throwable,
|
t: Throwable,
|
||||||
response: Response?,
|
response: Response?,
|
||||||
) {
|
) {
|
||||||
|
// Close the channel on failure, and propagate the error.
|
||||||
|
incomingMessages.close()
|
||||||
|
job.cancel()
|
||||||
|
scope.cancel()
|
||||||
|
|
||||||
socket = null
|
socket = null
|
||||||
out.onFailure(t, response?.code, response?.message)
|
out.onFailure(t, response?.code, response?.message)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -115,12 +115,13 @@ class NostrClient(
|
|||||||
socketBuilder = websocketBuilder,
|
socketBuilder = websocketBuilder,
|
||||||
listener = relayPool,
|
listener = relayPool,
|
||||||
stats = RelayStats.get(relay),
|
stats = RelayStats.get(relay),
|
||||||
scope = scope,
|
|
||||||
) { liveRelay ->
|
) { liveRelay ->
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
activeRequests.forEachSub(relay, liveRelay::sendRequest)
|
scope.launch(Dispatchers.Default) {
|
||||||
activeCounts.forEachSub(relay, liveRelay::sendCount)
|
activeRequests.forEachSub(relay, liveRelay::sendRequest)
|
||||||
eventOutbox.forEachUnsentEvent(relay, liveRelay::send)
|
activeCounts.forEachSub(relay, liveRelay::sendCount)
|
||||||
|
eventOutbox.forEachUnsentEvent(relay, liveRelay::send)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-19
@@ -51,9 +51,6 @@ import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
|||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.concurrent.atomics.AtomicBoolean
|
import kotlin.concurrent.atomics.AtomicBoolean
|
||||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
@@ -82,13 +79,11 @@ open class BasicRelayClient(
|
|||||||
val socketBuilder: WebsocketBuilder,
|
val socketBuilder: WebsocketBuilder,
|
||||||
val listener: IRelayClientListener,
|
val listener: IRelayClientListener,
|
||||||
val stats: RelayStat = RelayStat(),
|
val stats: RelayStat = RelayStat(),
|
||||||
val scope: CoroutineScope,
|
|
||||||
val defaultOnConnect: (BasicRelayClient) -> Unit = { },
|
val defaultOnConnect: (BasicRelayClient) -> Unit = { },
|
||||||
) : IRelayClient {
|
) : IRelayClient {
|
||||||
companion object {
|
companion object {
|
||||||
// minimum wait time to reconnect: 1 second
|
// minimum wait time to reconnect: 1 second
|
||||||
const val DELAY_TO_RECONNECT_IN_SECS = 1
|
const val DELAY_TO_RECONNECT_IN_SECS = 1
|
||||||
const val EVENT_MESSAGE_PREFIX = "[\"${EventMessage.LABEL}\""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val logTag = "Relay ${url.displayUrl()}"
|
private val logTag = "Relay ${url.displayUrl()}"
|
||||||
@@ -166,24 +161,13 @@ open class BasicRelayClient(
|
|||||||
|
|
||||||
markConnectionAsReady(pingMillis, compression)
|
markConnectionAsReady(pingMillis, compression)
|
||||||
|
|
||||||
scope.launch(Dispatchers.Default) {
|
onConnected()
|
||||||
onConnected()
|
|
||||||
}
|
|
||||||
|
|
||||||
listener.onRelayStateChange(this@BasicRelayClient, RelayState.CONNECTED)
|
listener.onRelayStateChange(this@BasicRelayClient, RelayState.CONNECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMessage(text: String) {
|
override fun onMessage(text: String) {
|
||||||
// Log.d(logTag, "Receiving: $text")
|
consumeIncomingMessage(text, onConnected)
|
||||||
|
|
||||||
if (text.startsWith(EVENT_MESSAGE_PREFIX)) {
|
|
||||||
// defers the parsing of ["EVENTS" to avoid blocking the HTTP thread
|
|
||||||
scope.launch(Dispatchers.Default) {
|
|
||||||
consumeIncomingCommand(text, onConnected)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
consumeIncomingCommand(text, onConnected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClosing(
|
override fun onClosing(
|
||||||
@@ -237,7 +221,7 @@ open class BasicRelayClient(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consumeIncomingCommand(
|
fun consumeIncomingMessage(
|
||||||
text: String,
|
text: String,
|
||||||
onConnected: () -> Unit,
|
onConnected: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|||||||
-3
@@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.basic.BasicRelayCl
|
|||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat
|
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This relay client saves any event that will be sent in an outbox
|
* This relay client saves any event that will be sent in an outbox
|
||||||
@@ -37,13 +36,11 @@ class SimpleRelayClient(
|
|||||||
socketBuilder: WebsocketBuilder,
|
socketBuilder: WebsocketBuilder,
|
||||||
listener: IRelayClientListener,
|
listener: IRelayClientListener,
|
||||||
stats: RelayStat = RelayStat(),
|
stats: RelayStat = RelayStat(),
|
||||||
scopeToParseEvents: CoroutineScope,
|
|
||||||
defaultOnConnect: (BasicRelayClient) -> Unit = { },
|
defaultOnConnect: (BasicRelayClient) -> Unit = { },
|
||||||
) : IRelayClient by BasicRelayClient(
|
) : IRelayClient by BasicRelayClient(
|
||||||
url,
|
url,
|
||||||
socketBuilder,
|
socketBuilder,
|
||||||
OutboxCache(listener),
|
OutboxCache(listener),
|
||||||
stats,
|
stats,
|
||||||
scopeToParseEvents,
|
|
||||||
defaultOnConnect,
|
defaultOnConnect,
|
||||||
)
|
)
|
||||||
|
|||||||
+48
-3
@@ -24,6 +24,14 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
|||||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
||||||
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -35,6 +43,14 @@ class BasicOkHttpWebSocket(
|
|||||||
val httpClient: (NormalizedRelayUrl) -> OkHttpClient,
|
val httpClient: (NormalizedRelayUrl) -> OkHttpClient,
|
||||||
val out: WebSocketListener,
|
val out: WebSocketListener,
|
||||||
) : WebSocket {
|
) : WebSocket {
|
||||||
|
companion object {
|
||||||
|
// Exists to avoid exceptions stopping the coroutine
|
||||||
|
val exceptionHandler =
|
||||||
|
CoroutineExceptionHandler { _, throwable ->
|
||||||
|
Log.e("BasicOkHttpWebSocket", "WebsocketListener Caught exception: ${throwable.message}", throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var socket: OkHttpWebSocket? = null
|
private var socket: OkHttpWebSocket? = null
|
||||||
|
|
||||||
override fun needsReconnect() = socket == null
|
override fun needsReconnect() = socket == null
|
||||||
@@ -44,6 +60,15 @@ class BasicOkHttpWebSocket(
|
|||||||
|
|
||||||
val listener =
|
val listener =
|
||||||
object : OkHttpWebSocketListener() {
|
object : OkHttpWebSocketListener() {
|
||||||
|
val scope = CoroutineScope(Dispatchers.Default + exceptionHandler)
|
||||||
|
val incomingMessages: Channel<String> = Channel(Channel.UNLIMITED)
|
||||||
|
val job = // Launch a coroutine to process messages from the channel.
|
||||||
|
scope.launch {
|
||||||
|
for (message in incomingMessages) {
|
||||||
|
out.onMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onOpen(
|
override fun onOpen(
|
||||||
webSocket: OkHttpWebSocket,
|
webSocket: OkHttpWebSocket,
|
||||||
response: Response,
|
response: Response,
|
||||||
@@ -55,7 +80,13 @@ class BasicOkHttpWebSocket(
|
|||||||
override fun onMessage(
|
override fun onMessage(
|
||||||
webSocket: OkHttpWebSocket,
|
webSocket: OkHttpWebSocket,
|
||||||
text: String,
|
text: String,
|
||||||
) = out.onMessage(text)
|
) {
|
||||||
|
Log.d("OkHttpWebsocketListener", "Processing: $text")
|
||||||
|
// Asynchronously send the received message to the channel.
|
||||||
|
// `trySendBlocking` is used here for simplicity within the callback,
|
||||||
|
// but it's important to understand potential thread blocking if the buffer is full.
|
||||||
|
incomingMessages.trySendBlocking(text)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onClosing(
|
override fun onClosing(
|
||||||
webSocket: OkHttpWebSocket,
|
webSocket: OkHttpWebSocket,
|
||||||
@@ -67,13 +98,27 @@ class BasicOkHttpWebSocket(
|
|||||||
webSocket: OkHttpWebSocket,
|
webSocket: OkHttpWebSocket,
|
||||||
code: Int,
|
code: Int,
|
||||||
reason: String,
|
reason: String,
|
||||||
) = out.onClosed(code, reason)
|
) {
|
||||||
|
// Close the channel when the WebSocket connection is closed.
|
||||||
|
incomingMessages.close()
|
||||||
|
job.cancel()
|
||||||
|
scope.cancel()
|
||||||
|
|
||||||
|
out.onClosed(code, reason)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFailure(
|
override fun onFailure(
|
||||||
webSocket: OkHttpWebSocket,
|
webSocket: OkHttpWebSocket,
|
||||||
t: Throwable,
|
t: Throwable,
|
||||||
response: Response?,
|
response: Response?,
|
||||||
) = out.onFailure(t, response?.code, response?.message)
|
) {
|
||||||
|
// Close the channel on failure, and propagate the error.
|
||||||
|
incomingMessages.close()
|
||||||
|
job.cancel()
|
||||||
|
scope.cancel()
|
||||||
|
|
||||||
|
out.onFailure(t, response?.code, response?.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
socket = httpClient(url).newWebSocket(request, listener)
|
socket = httpClient(url).newWebSocket(request, listener)
|
||||||
|
|||||||
Reference in New Issue
Block a user