From 669199ab7e91e3a1d806976943f26b6917cd3017 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 18 May 2026 21:21:02 +0000 Subject: [PATCH] refactor(quartz): use LargeCache for RelayAuthenticator authStatus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2946 fixed the ClassCastException with a bespoke AtomicReference + CAS copy-on-write helper. Quartz already has a concurrent-map abstraction for exactly this purpose — LargeCache — with platform-tuned actuals (ConcurrentSkipListMap on jvmAndroid, CacheMap on Apple, custom on Linux). Swap to it. Removes the bespoke putAuthStatus/removeAuthStatus helpers, the ExperimentalAtomicApi opt-in, and the AtomicReference imports. The RelayAuthenticatorConcurrencyTest from #2946 still passes against the new implementation. --- .../relay/client/auth/RelayAuthenticator.kt | 40 +++++-------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticator.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticator.kt index 846469de0b..848b3f810f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticator.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/auth/RelayAuthenticator.kt @@ -31,13 +31,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.cache.LargeCache import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import kotlin.concurrent.atomics.AtomicReference -import kotlin.concurrent.atomics.ExperimentalAtomicApi interface IAuthStatus { fun hasFinishedAuthentication(relay: NormalizedRelayUrl): Boolean @@ -47,36 +46,15 @@ object EmptyIAuthStatus : IAuthStatus { override fun hasFinishedAuthentication(relay: NormalizedRelayUrl) = true } -@OptIn(ExperimentalAtomicApi::class) class RelayAuthenticator( val client: INostrClient, val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()), val signWithAllLoggedInUsers: suspend (EventTemplate) -> List, ) : IAuthStatus { // Connection callbacks fire on the per-relay OkHttp dispatcher thread, so - // this state is mutated concurrently — copy-on-write under AtomicReference. - private val authStatus: AtomicReference> = - AtomicReference(emptyMap()) - - private fun putAuthStatus( - relay: NormalizedRelayUrl, - status: RelayAuthStatus, - ) { - while (true) { - val current = authStatus.load() - val next = current + (relay to status) - if (authStatus.compareAndSet(current, next)) return - } - } - - private fun removeAuthStatus(relay: NormalizedRelayUrl) { - while (true) { - val current = authStatus.load() - if (relay !in current) return - val next = current - relay - if (authStatus.compareAndSet(current, next)) return - } - } + // this state is mutated concurrently — LargeCache wraps a platform-tuned + // concurrent map (ConcurrentSkipListMap on jvmAndroid, CacheMap on Apple). + private val authStatus = LargeCache() private val clientListener = object : RelayConnectionListener { @@ -92,11 +70,11 @@ class RelayAuthenticator( } override fun onConnecting(relay: IRelayClient) { - putAuthStatus(relay.url, RelayAuthStatus()) + authStatus.put(relay.url, RelayAuthStatus()) } override fun onDisconnected(relay: IRelayClient) { - removeAuthStatus(relay.url) + authStatus.remove(relay.url) } } @@ -108,7 +86,7 @@ class RelayAuthenticator( val ev = RelayAuthEvent.build(relay.url, msg.challenge) signWithAllLoggedInUsers(ev).forEach { authEvent -> // only send replies to new challenges to avoid infinite loop: - if (authStatus.load()[relay.url]?.saveAuthSubmission(authEvent) == true) { + if (authStatus.get(relay.url)?.saveAuthSubmission(authEvent) == true) { relay.sendIfConnected(AuthCmd(authEvent)) } } @@ -120,12 +98,12 @@ class RelayAuthenticator( msg: OkMessage, ) { // if this is the OK of an auth event, renew all subscriptions and resend all outgoing events. - if (authStatus.load()[relay.url]?.checkAuthResults(msg.eventId, msg.success) == true) { + if (authStatus.get(relay.url)?.checkAuthResults(msg.eventId, msg.success) == true) { client.syncFilters(relay) } } - override fun hasFinishedAuthentication(relay: NormalizedRelayUrl) = authStatus.load()[relay]?.hasFinishedAllAuths() != false + override fun hasFinishedAuthentication(relay: NormalizedRelayUrl) = authStatus.get(relay)?.hasFinishedAllAuths() != false init { Log.d("RelayAuthenticator", "Init, Subscribe")