- Bugfix to mark relay connections as AfterEOSE when the relay client or server closes the subscription

- Adds an arrival time to log the time EOSEs were received in case the listener is running async processes later.
This commit is contained in:
Vitor Pamplona
2025-05-13 14:43:28 -04:00
parent 2d5732bcce
commit 50cde1bc3e
9 changed files with 47 additions and 50 deletions

View File

@@ -42,6 +42,7 @@ class RelayLogger(
event: Event, event: Event,
subscriptionId: String, subscriptionId: String,
relay: Relay, relay: Relay,
arrivalTime: Long,
afterEOSE: Boolean, afterEOSE: Boolean,
) { ) {
Log.d(TAG, "Relay onEVENT ${relay.url} ($subscriptionId - $afterEOSE) ${event.toJson()}") Log.d(TAG, "Relay onEVENT ${relay.url} ($subscriptionId - $afterEOSE) ${event.toJson()}")

View File

@@ -46,6 +46,7 @@ class RelaySpeedLogger(
event: Event, event: Event,
subscriptionId: String, subscriptionId: String,
relay: Relay, relay: Relay,
arrivalTime: Long,
afterEOSE: Boolean, afterEOSE: Boolean,
) { ) {
val now = TimeUtils.now() / 10 val now = TimeUtils.now() / 10

View File

@@ -25,12 +25,9 @@ import com.vitorpamplona.ammolite.service.checkNotInMainThread
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.RelayState import com.vitorpamplona.quartz.nip01Core.relay.RelayState
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.UUID import java.util.UUID
@@ -42,13 +39,6 @@ import java.util.concurrent.TimeUnit
*/ */
class NostrClient( class NostrClient(
private val websocketBuilder: WebsocketBuilderFactory, private val websocketBuilder: WebsocketBuilderFactory,
private val listenerScope: CoroutineScope =
CoroutineScope(
Dispatchers.Default + SupervisorJob() +
CoroutineExceptionHandler { _, throwable ->
Log.e("NostrClient", "Caught exception: ${throwable.message}", throwable)
},
),
) : RelayPool.Listener { ) : RelayPool.Listener {
private val relayPool: RelayPool = RelayPool() private val relayPool: RelayPool = RelayPool()
private val activeSubscriptions: MutableSubscriptionManager = MutableSubscriptionManager() private val activeSubscriptions: MutableSubscriptionManager = MutableSubscriptionManager()
@@ -135,12 +125,14 @@ class NostrClient(
event: Event, event: Event,
subId: String, subId: String,
relay: Relay, relay: Relay,
arrivalTime: Long,
afterEOSE: Boolean, afterEOSE: Boolean,
) { ) {
if (subId == subscriptionId) { if (subId == subscriptionId) {
onResponse(event)
unsubscribe(this) unsubscribe(this)
close(subscriptionId) close(subscriptionId)
onResponse(event)
} }
} }
}, },
@@ -186,6 +178,7 @@ class NostrClient(
override fun onEOSE( override fun onEOSE(
relay: Relay, relay: Relay,
subscriptionId: String, subscriptionId: String,
arrivalTime: Long,
) { ) {
latch.countDown() latch.countDown()
Log.d("sendAndWaitForResponse", "onEOSE relay ${relay.url} count: ${latch.count}") Log.d("sendAndWaitForResponse", "onEOSE relay ${relay.url} count: ${latch.count}")
@@ -320,32 +313,28 @@ class NostrClient(
event: Event, event: Event,
subscriptionId: String, subscriptionId: String,
relay: Relay, relay: Relay,
arrivalTime: Long,
afterEOSE: Boolean, afterEOSE: Boolean,
) { ) {
// Releases the Web thread for the new payload. // Releases the Web thread for the new payload.
// May need to add a processing queue if processing new events become too costly. // May need to add a processing queue if processing new events become too costly.
listenerScope.launch { listeners.forEach { it.onEvent(event, subscriptionId, relay, arrivalTime, afterEOSE) }
listeners.forEach { it.onEvent(event, subscriptionId, relay, afterEOSE) }
}
} }
override fun onEOSE( override fun onEOSE(
relay: Relay, relay: Relay,
subscriptionId: String, subscriptionId: String,
arrivalTime: Long,
) { ) {
listenerScope.launch { listeners.forEach { it.onEOSE(relay, subscriptionId, arrivalTime) }
listeners.forEach { it.onEOSE(relay, subscriptionId) }
}
} }
override fun onRelayStateChange( override fun onRelayStateChange(
type: RelayState, type: RelayState,
relay: Relay, relay: Relay,
) { ) {
listenerScope.launch {
listeners.forEach { it.onRelayStateChange(type, relay) } listeners.forEach { it.onRelayStateChange(type, relay) }
} }
}
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
override fun onSendResponse( override fun onSendResponse(
@@ -356,10 +345,8 @@ class NostrClient(
) { ) {
// Releases the Web thread for the new payload. // Releases the Web thread for the new payload.
// May need to add a processing queue if processing new events become too costly. // May need to add a processing queue if processing new events become too costly.
listenerScope.launch {
listeners.forEach { it.onSendResponse(eventId, success, message, relay) } listeners.forEach { it.onSendResponse(eventId, success, message, relay) }
} }
}
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
override fun onAuth( override fun onAuth(
@@ -368,7 +355,7 @@ class NostrClient(
) { ) {
// Releases the Web thread for the new payload. // Releases the Web thread for the new payload.
// May need to add a processing queue if processing new events become too costly. // May need to add a processing queue if processing new events become too costly.
listenerScope.launch { listeners.forEach { it.onAuth(relay, challenge) } } listeners.forEach { it.onAuth(relay, challenge) }
} }
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
@@ -378,10 +365,8 @@ class NostrClient(
) { ) {
// Releases the Web thread for the new payload. // Releases the Web thread for the new payload.
// May need to add a processing queue if processing new events become too costly. // May need to add a processing queue if processing new events become too costly.
listenerScope.launch {
listeners.forEach { it.onNotify(relay, description) } listeners.forEach { it.onNotify(relay, description) }
} }
}
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
override fun onSend( override fun onSend(
@@ -389,20 +374,16 @@ class NostrClient(
msg: String, msg: String,
success: Boolean, success: Boolean,
) { ) {
listenerScope.launch {
listeners.forEach { it.onSend(relay, msg, success) } listeners.forEach { it.onSend(relay, msg, success) }
} }
}
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
override fun onBeforeSend( override fun onBeforeSend(
relay: Relay, relay: Relay,
event: Event, event: Event,
) { ) {
listenerScope.launch {
listeners.forEach { it.onBeforeSend(relay, event) } listeners.forEach { it.onBeforeSend(relay, event) }
} }
}
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
override fun onError( override fun onError(
@@ -410,10 +391,8 @@ class NostrClient(
subscriptionId: String, subscriptionId: String,
relay: Relay, relay: Relay,
) { ) {
listenerScope.launch {
listeners.forEach { it.onError(error, subscriptionId, relay) } listeners.forEach { it.onError(error, subscriptionId, relay) }
} }
}
fun subscribe(listener: Listener) { fun subscribe(listener: Listener) {
listeners = listeners.plus(listener) listeners = listeners.plus(listener)
@@ -439,6 +418,7 @@ class NostrClient(
event: Event, event: Event,
subscriptionId: String, subscriptionId: String,
relay: Relay, relay: Relay,
arrivalTime: Long,
afterEOSE: Boolean, afterEOSE: Boolean,
) = Unit ) = Unit
@@ -446,6 +426,7 @@ class NostrClient(
open fun onEOSE( open fun onEOSE(
relay: Relay, relay: Relay,
subscriptionId: String, subscriptionId: String,
arrivalTime: Long,
) = Unit ) = Unit
/** Connected to or disconnected from a relay */ /** Connected to or disconnected from a relay */

View File

@@ -161,8 +161,9 @@ class Relay(
relay: SimpleClientRelay, relay: SimpleClientRelay,
subscriptionId: String, subscriptionId: String,
event: Event, event: Event,
time: Long,
afterEOSE: Boolean, afterEOSE: Boolean,
) = listeners.forEach { it.onEvent(this, subscriptionId, event, afterEOSE) } ) = listeners.forEach { it.onEvent(this, subscriptionId, event, time, afterEOSE) }
override fun onError( override fun onError(
relay: SimpleClientRelay, relay: SimpleClientRelay,
@@ -173,7 +174,8 @@ class Relay(
override fun onEOSE( override fun onEOSE(
relay: SimpleClientRelay, relay: SimpleClientRelay,
subscriptionId: String, subscriptionId: String,
) = listeners.forEach { it.onEOSE(this, subscriptionId) } time: Long,
) = listeners.forEach { it.onEOSE(this, subscriptionId, time) }
override fun onRelayStateChange( override fun onRelayStateChange(
relay: SimpleClientRelay, relay: SimpleClientRelay,
@@ -219,12 +221,14 @@ class Relay(
relay: Relay, relay: Relay,
subscriptionId: String, subscriptionId: String,
event: Event, event: Event,
time: Long,
afterEOSE: Boolean, afterEOSE: Boolean,
) )
fun onEOSE( fun onEOSE(
relay: Relay, relay: Relay,
subscriptionId: String, subscriptionId: String,
time: Long,
) )
fun onError( fun onError(

View File

@@ -170,12 +170,14 @@ class RelayPool : Relay.Listener {
event: Event, event: Event,
subscriptionId: String, subscriptionId: String,
relay: Relay, relay: Relay,
arrivalTime: Long,
afterEOSE: Boolean, afterEOSE: Boolean,
) )
fun onEOSE( fun onEOSE(
relay: Relay, relay: Relay,
subscriptionId: String, subscriptionId: String,
arrivalTime: Long,
) )
fun onRelayStateChange( fun onRelayStateChange(
@@ -222,9 +224,10 @@ class RelayPool : Relay.Listener {
relay: Relay, relay: Relay,
subscriptionId: String, subscriptionId: String,
event: Event, event: Event,
arrivalTime: Long,
afterEOSE: Boolean, afterEOSE: Boolean,
) { ) {
listeners.forEach { it.onEvent(event, subscriptionId, relay, afterEOSE) } listeners.forEach { it.onEvent(event, subscriptionId, relay, arrivalTime, afterEOSE) }
} }
override fun onError( override fun onError(
@@ -239,8 +242,9 @@ class RelayPool : Relay.Listener {
override fun onEOSE( override fun onEOSE(
relay: Relay, relay: Relay,
subscriptionId: String, subscriptionId: String,
arrivalTime: Long,
) { ) {
listeners.forEach { it.onEOSE(relay, subscriptionId) } listeners.forEach { it.onEOSE(relay, subscriptionId, arrivalTime) }
updateStatus() updateStatus()
} }

View File

@@ -38,6 +38,7 @@ class EventCollector(
event: Event, event: Event,
subscriptionId: String, subscriptionId: String,
relay: Relay, relay: Relay,
arrivalTime: Long,
afterEOSE: Boolean, afterEOSE: Boolean,
) { ) {
onEvent(event, relay) onEvent(event, relay)

View File

@@ -27,7 +27,6 @@ import com.vitorpamplona.ammolite.relays.Relay
import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.ammolite.relays.TypedFilter
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.RelayState import com.vitorpamplona.quartz.nip01Core.relay.RelayState
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -55,6 +54,7 @@ abstract class NostrDataSource(
event: Event, event: Event,
subscriptionId: String, subscriptionId: String,
relay: Relay, relay: Relay,
arrivalTime: Long,
afterEOSE: Boolean, afterEOSE: Boolean,
) { ) {
if (subscriptions.contains(subscriptionId)) { if (subscriptions.contains(subscriptionId)) {
@@ -63,7 +63,7 @@ abstract class NostrDataSource(
consume(event, relay) consume(event, relay)
if (afterEOSE) { if (afterEOSE) {
markAsEOSE(subscriptionId, relay) markAsEOSE(subscriptionId, relay, arrivalTime)
} }
} }
} }
@@ -71,9 +71,10 @@ abstract class NostrDataSource(
override fun onEOSE( override fun onEOSE(
relay: Relay, relay: Relay,
subscriptionId: String, subscriptionId: String,
arrivalTime: Long,
) { ) {
if (subscriptions.contains(subscriptionId)) { if (subscriptions.contains(subscriptionId)) {
markAsEOSE(subscriptionId, relay) markAsEOSE(subscriptionId, relay, arrivalTime)
} }
} }
@@ -253,10 +254,10 @@ abstract class NostrDataSource(
open fun markAsEOSE( open fun markAsEOSE(
subscriptionId: String, subscriptionId: String,
relay: Relay, relay: Relay,
arrivalTime: Long,
) { ) {
subscriptions[subscriptionId]?.callEose( subscriptions[subscriptionId]?.callEose(
// in case people's clock is slighly off. arrivalTime,
TimeUtils.oneMinuteAgo(),
relay.url, relay.url,
) )
} }

View File

@@ -39,6 +39,7 @@ class RelayLogger(
event: Event, event: Event,
subscriptionId: String, subscriptionId: String,
relay: Relay, relay: Relay,
arrivalTime: Long,
afterEOSE: Boolean, afterEOSE: Boolean,
) { ) {
Log.d("Relay", "Relay onEVENT ${relay.url} ($subscriptionId - $afterEOSE) ${event.toJson()}") Log.d("Relay", "Relay onEVENT ${relay.url} ($subscriptionId - $afterEOSE) ${event.toJson()}")

View File

@@ -234,13 +234,12 @@ class SimpleClientRelay(
fun processNewRelayMessage(newMessage: String) { fun processNewRelayMessage(newMessage: String) {
when (val msg = parser.parse(newMessage)) { when (val msg = parser.parse(newMessage)) {
is EventMessage -> { is EventMessage -> {
// Log.w("Relay", "Relay onEVENT $url $newMessage") listener.onEvent(this, msg.subId, msg.event, TimeUtils.now(), afterEOSEPerSubscription[msg.subId] == true)
listener.onEvent(this, msg.subId, msg.event, afterEOSEPerSubscription[msg.subId] == true)
} }
is EoseMessage -> { is EoseMessage -> {
// Log.w("Relay", "Relay onEOSE $url $newMessage") // Log.w("Relay", "Relay onEOSE $url $newMessage")
afterEOSEPerSubscription[msg.subId] = true afterEOSEPerSubscription[msg.subId] = true
listener.onEOSE(this@SimpleClientRelay, msg.subId) listener.onEOSE(this, msg.subId, TimeUtils.now())
} }
is NoticeMessage -> { is NoticeMessage -> {
// Log.w("Relay", "Relay onNotice $url, $newMessage") // Log.w("Relay", "Relay onNotice $url, $newMessage")
@@ -282,6 +281,7 @@ class SimpleClientRelay(
listener.onNotify(this@SimpleClientRelay, msg.message) listener.onNotify(this@SimpleClientRelay, msg.message)
} }
is ClosedMessage -> { is ClosedMessage -> {
afterEOSEPerSubscription[msg.subscriptionId] = false
// Log.w("Relay", "Relay Closed Subscription $url, $newMessage") // Log.w("Relay", "Relay Closed Subscription $url, $newMessage")
listener.onClosed(this@SimpleClientRelay, msg.subscriptionId, msg.message) listener.onClosed(this@SimpleClientRelay, msg.subscriptionId, msg.message)
} }
@@ -318,8 +318,8 @@ class SimpleClientRelay(
if (isConnectionStarted()) { if (isConnectionStarted()) {
if (isReady) { if (isReady) {
if (filters.isNotEmpty()) { if (filters.isNotEmpty()) {
writeToSocket(ReqCmd.toJson(requestId, filters))
afterEOSEPerSubscription[requestId] = false afterEOSEPerSubscription[requestId] = false
writeToSocket(ReqCmd.toJson(requestId, filters))
} }
} }
} else { } else {
@@ -339,8 +339,8 @@ class SimpleClientRelay(
if (isConnectionStarted()) { if (isConnectionStarted()) {
if (isReady) { if (isReady) {
if (filters.isNotEmpty()) { if (filters.isNotEmpty()) {
writeToSocket(CountCmd.toJson(requestId, filters))
afterEOSEPerSubscription[requestId] = false afterEOSEPerSubscription[requestId] = false
writeToSocket(CountCmd.toJson(requestId, filters))
} }
} }
} else { } else {
@@ -423,6 +423,7 @@ class SimpleClientRelay(
fun close(subscriptionId: String) { fun close(subscriptionId: String) {
writeToSocket(CloseCmd.toJson(subscriptionId)) writeToSocket(CloseCmd.toJson(subscriptionId))
afterEOSEPerSubscription[subscriptionId] = false
} }
interface Listener { interface Listener {
@@ -433,6 +434,7 @@ class SimpleClientRelay(
relay: SimpleClientRelay, relay: SimpleClientRelay,
subscriptionId: String, subscriptionId: String,
event: Event, event: Event,
arrivalTime: Long,
afterEOSE: Boolean, afterEOSE: Boolean,
) )
@@ -442,6 +444,7 @@ class SimpleClientRelay(
fun onEOSE( fun onEOSE(
relay: SimpleClientRelay, relay: SimpleClientRelay,
subscriptionId: String, subscriptionId: String,
arrivalTime: Long,
) )
/** /**