mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
feat(relays): merge account metadata across accounts into one REQ
Account metadata was the largest single contributor to blowing a relay's subscription cap: seven filters in a subscription, repeated once per logged-in account. Every one of them is `authors`-keyed, so a relay that several accounts read from can be asked about all of them by widening `authors` — the same shape of merge the notification tail just took, and for the same reason. The per-account `limit`s are summed rather than shared. These are mostly replaceable events, so the limit is a safety bound rather than a page size, and scaling it by the number of accounts leaves each one exactly the headroom it had alone. That is the difference from gift wraps, which stay per-account because their `limit` IS a page size over unsolicited content. Measured on emulator-5554, four accounts, cold start, counting nos.lol's `ERROR: too many concurrent REQs`: 13 before either merge, 11 after notifications, 8 after this. The subscription count on that relay went from 24-26 to 23, against its cap of 20. So the per-account multiplication is no longer the driver, and the remaining 23 are not account-level at all — they are the feed, channel and finder assemblers. Cutting further means looking there, not at more merging. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
24decefd4b
commit
fe2d7ef045
+66
-36
@@ -20,66 +20,96 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Each account's own profile, lists and recent posts — for **every** logged-in account, in one
|
||||
* subscription.
|
||||
*
|
||||
* Every filter here is `authors`-keyed, so a relay that several accounts read from can be asked
|
||||
* about all of them at once by widening `authors` rather than opening a REQ per account. This was
|
||||
* the largest single contributor to blowing a relay's `max_subscriptions`: seven filters in a
|
||||
* subscription, repeated once per account.
|
||||
*
|
||||
* The per-account `limit`s are summed rather than shared. These are mostly replaceable events, so
|
||||
* the limit is a safety bound rather than a page size, and scaling it by the number of accounts
|
||||
* keeps each one exactly the headroom it had alone.
|
||||
*/
|
||||
class AccountMetadataEoseManager(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<AccountQueryState>,
|
||||
) : PerUserEoseManager<AccountQueryState>(client, allKeys) {
|
||||
override fun user(key: AccountQueryState) = key.account.userProfile()
|
||||
) : SingleSubEoseManager<AccountQueryState>(client, allKeys) {
|
||||
override fun distinct(key: AccountQueryState) = key.account.userProfile()
|
||||
|
||||
fun relayFlow(query: AccountQueryState) = query.account.homeRelays.flow
|
||||
|
||||
override fun updateFilter(
|
||||
key: AccountQueryState,
|
||||
keys: List<AccountQueryState>,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> =
|
||||
relayFlow(key).value.flatMap {
|
||||
val since = since?.get(it)?.time
|
||||
listOf(
|
||||
filterAccountInfoAndListsFromKey(it, user(key).pubkeyHex, since),
|
||||
filterFollowsAndMutesFromKey(it, user(key).pubkeyHex, since),
|
||||
filterBookmarksAndReportsFromKey(it, user(key).pubkeyHex, since),
|
||||
filterLastPostsFromKey(it, user(key).pubkeyHex, since ?: TimeUtils.oneMonthAgo()),
|
||||
filterBasicAccountInfoFromKeys(it, key.otherAccounts.minus(key.account.userProfile().pubkeyHex).toList(), since),
|
||||
).flatten()
|
||||
): List<RelayBasedFilter> {
|
||||
val accountsPerRelay = mutableMapOf<NormalizedRelayUrl, MutableList<AccountQueryState>>()
|
||||
keys.forEach { key ->
|
||||
relayFlow(key).value.forEach { relay ->
|
||||
accountsPerRelay.getOrPut(relay) { mutableListOf() }.add(key)
|
||||
}
|
||||
}
|
||||
|
||||
val userJobMap = mutableMapOf<User, List<Job>>()
|
||||
return accountsPerRelay.flatMap { (relay, accounts) ->
|
||||
val pubkeys = accounts.map { it.account.userProfile().pubkeyHex }
|
||||
val relaySince = since?.get(relay)?.time
|
||||
|
||||
// The account-switcher avatars: other logged-in accounts this screen wants to name.
|
||||
// Screens supply them; the background registry does not, so this is usually empty.
|
||||
val otherAccounts = accounts.flatMapTo(mutableSetOf()) { it.otherAccounts }.minus(pubkeys.toSet())
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
override fun newSub(key: AccountQueryState): Subscription {
|
||||
val user = user(key)
|
||||
userJobMap[user]?.forEach { it.cancel() }
|
||||
userJobMap[user] =
|
||||
listOf(
|
||||
key.account.scope.launch(Dispatchers.IO) {
|
||||
relayFlow(key).collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return super.newSub(key)
|
||||
filterAccountInfoAndListsFromKey(relay, pubkeys, relaySince),
|
||||
filterFollowsAndMutesFromKey(relay, pubkeys, relaySince),
|
||||
filterBookmarksAndReportsFromKey(relay, pubkeys, relaySince),
|
||||
filterLastPostsFromKey(relay, pubkeys, relaySince ?: TimeUtils.oneMonthAgo()),
|
||||
filterBasicAccountInfoFromKeys(relay, otherAccounts.toList(), relaySince),
|
||||
).flatten()
|
||||
}
|
||||
}
|
||||
|
||||
override fun endSub(
|
||||
key: User,
|
||||
subId: String,
|
||||
) {
|
||||
super.endSub(key, subId)
|
||||
userJobMap[key]?.forEach { it.cancel() }
|
||||
/** Per-account relay watchers, reconciled as accounts come and go. See the notifications manager. */
|
||||
private val userJobMap = mutableMapOf<User, List<Job>>()
|
||||
|
||||
override fun updateSubscriptions(keys: Set<AccountQueryState>) {
|
||||
val wanted = keys.associateBy { it.account.userProfile() }
|
||||
|
||||
(userJobMap.keys - wanted.keys).toList().forEach { user ->
|
||||
userJobMap.remove(user)?.forEach { it.cancel() }
|
||||
}
|
||||
|
||||
wanted.forEach { (user, key) ->
|
||||
if (user !in userJobMap) {
|
||||
userJobMap[user] =
|
||||
listOf(
|
||||
key.account.scope.launch(Dispatchers.IO) {
|
||||
relayFlow(key).collectLatest { invalidateFilters() }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
super.updateSubscriptions(keys)
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
userJobMap.values.forEach { jobs -> jobs.forEach { it.cancel() } }
|
||||
userJobMap.clear()
|
||||
super.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -109,10 +109,10 @@ val AmethystMetadataTagMapFilter = mapOf("d" to listOf(APP_SPECIFIC_DATA_D_TAG))
|
||||
|
||||
fun filterAccountInfoAndListsFromKey(
|
||||
relay: NormalizedRelayUrl,
|
||||
pubkey: HexKey,
|
||||
pubkeys: List<HexKey>,
|
||||
since: Long?,
|
||||
): List<RelayBasedFilter> {
|
||||
if (pubkey.isEmpty()) return emptyList()
|
||||
if (pubkeys.isEmpty()) return emptyList()
|
||||
|
||||
return listOf(
|
||||
RelayBasedFilter(
|
||||
@@ -121,8 +121,8 @@ fun filterAccountInfoAndListsFromKey(
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.ACCOUNT_DATA,
|
||||
kinds = AccountInfoAndListsFromKeyKinds,
|
||||
authors = listOf(pubkey),
|
||||
limit = 20,
|
||||
authors = pubkeys,
|
||||
limit = 20 * pubkeys.size,
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
@@ -132,8 +132,8 @@ fun filterAccountInfoAndListsFromKey(
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.ACCOUNT_DATA,
|
||||
kinds = AccountInfoAndListsFromKeyKinds2,
|
||||
authors = listOf(pubkey),
|
||||
limit = 80,
|
||||
authors = pubkeys,
|
||||
limit = 80 * pubkeys.size,
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
@@ -141,7 +141,7 @@ fun filterAccountInfoAndListsFromKey(
|
||||
// Addressable — one card per target user — hence its own larger-limit filter.
|
||||
filterContactCardsByAuthorInTheRelay(
|
||||
relay = relay,
|
||||
author = pubkey,
|
||||
authors = pubkeys,
|
||||
since = since,
|
||||
),
|
||||
RelayBasedFilter(
|
||||
@@ -150,9 +150,9 @@ fun filterAccountInfoAndListsFromKey(
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.ACCOUNT_DATA,
|
||||
kinds = AmethystMetadataKinds,
|
||||
authors = listOf(pubkey),
|
||||
authors = pubkeys,
|
||||
tags = AmethystMetadataTagMapFilter,
|
||||
limit = 1,
|
||||
limit = 1 * pubkeys.size,
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
|
||||
+3
-3
@@ -44,10 +44,10 @@ val ReportsAndBookmarksFromKeyKinds =
|
||||
|
||||
fun filterBookmarksAndReportsFromKey(
|
||||
relay: NormalizedRelayUrl,
|
||||
pubkey: HexKey?,
|
||||
pubkeys: List<HexKey>,
|
||||
since: Long?,
|
||||
): List<RelayBasedFilter> {
|
||||
if (pubkey.isNullOrEmpty()) return emptyList()
|
||||
if (pubkeys.isEmpty()) return emptyList()
|
||||
|
||||
return listOf(
|
||||
RelayBasedFilter(
|
||||
@@ -56,7 +56,7 @@ fun filterBookmarksAndReportsFromKey(
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.ACCOUNT_DATA,
|
||||
kinds = ReportsAndBookmarksFromKeyKinds,
|
||||
authors = listOf(pubkey),
|
||||
authors = pubkeys,
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
|
||||
+4
-4
@@ -48,10 +48,10 @@ val FollowAndMutesFromKeyKinds =
|
||||
|
||||
fun filterFollowsAndMutesFromKey(
|
||||
relay: NormalizedRelayUrl,
|
||||
pubkey: HexKey,
|
||||
pubkeys: List<HexKey>,
|
||||
since: Long?,
|
||||
): List<RelayBasedFilter> {
|
||||
if (pubkey.isEmpty()) return emptyList()
|
||||
if (pubkeys.isEmpty()) return emptyList()
|
||||
|
||||
return listOf(
|
||||
RelayBasedFilter(
|
||||
@@ -60,8 +60,8 @@ fun filterFollowsAndMutesFromKey(
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.ACCOUNT_DATA,
|
||||
kinds = FollowAndMutesFromKeyKinds,
|
||||
authors = listOf(pubkey),
|
||||
limit = 100,
|
||||
authors = pubkeys,
|
||||
limit = 100 * pubkeys.size,
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
|
||||
+4
-4
@@ -28,10 +28,10 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
fun filterLastPostsFromKey(
|
||||
relay: NormalizedRelayUrl,
|
||||
pubkey: HexKey,
|
||||
pubkeys: List<HexKey>,
|
||||
since: Long?,
|
||||
): List<RelayBasedFilter> {
|
||||
if (pubkey.isEmpty()) return emptyList()
|
||||
if (pubkeys.isEmpty()) return emptyList()
|
||||
|
||||
return listOf(
|
||||
RelayBasedFilter(
|
||||
@@ -39,8 +39,8 @@ fun filterLastPostsFromKey(
|
||||
filter =
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.ACCOUNT_DATA,
|
||||
authors = listOf(pubkey),
|
||||
limit = 100,
|
||||
authors = pubkeys,
|
||||
limit = 100 * pubkeys.size,
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
|
||||
+3
-3
@@ -65,7 +65,7 @@ fun filterContactCardsToTargetKeysFromTrustedAccountsInTheRelay(
|
||||
*/
|
||||
fun filterContactCardsByAuthorInTheRelay(
|
||||
relay: NormalizedRelayUrl,
|
||||
author: HexKey,
|
||||
authors: List<HexKey>,
|
||||
since: Long?,
|
||||
limit: Int = 500,
|
||||
): RelayBasedFilter =
|
||||
@@ -75,9 +75,9 @@ fun filterContactCardsByAuthorInTheRelay(
|
||||
ExplainedFilter(
|
||||
purpose = SubPurpose.PROFILE_METADATA,
|
||||
// This variant fetches an account's OWN contact card, so the author is the owner.
|
||||
accountPubKeys = listOfNotNull(author),
|
||||
accountPubKeys = authors,
|
||||
kinds = ContactCardKindList,
|
||||
authors = listOf(author),
|
||||
authors = authors,
|
||||
limit = limit,
|
||||
since = since,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user