* 'main' of https://github.com/vitorpamplona/amethyst:
  optimise imports
  synchronize all cache mutations and supply sinceRelaySet so callers get snapshot copies rework groupByRelayPresence to build relay snapshots via sinceRelaySet,     filtering with immutable lists to prevent ConcurrentModificationException when relays update mid-iteration
  Hardened EOSEAccountFast against concurrent access so callers no longer iterate over live mutable maps
  Adjusted subscription cleanup to avoid mutating the watcher map while iterating it, preventing the ConcurrentModificationException when accounts switch

# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt
This commit is contained in:
Vitor Pamplona
2025-11-06 15:15:34 -05:00
7 changed files with 51 additions and 29 deletions

View File

@@ -46,7 +46,6 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
import kotlin.collections.map
/**
* Maintains several stateflows for each step in processing PeopleLists

View File

@@ -186,11 +186,11 @@ class AccountFollowsLoaderSubAssembler(
}
// removes accounts that are not being subscribed anymore.
accountUpdatesJobMap.forEach {
if (it.key !in uniqueSubscribedAccounts.keys) {
endWatcher(it.key)
}
}
// Cancel watchers for accounts no longer observed using a snapshot to avoid CME
accountUpdatesJobMap.keys
.toList()
.filter { it !in uniqueSubscribedAccounts.keys }
.forEach { endWatcher(it) }
}
private val accountUpdatesJobMap = mutableMapOf<User, Job>()

View File

@@ -90,19 +90,30 @@ class UserReportsSubAssembler(
users: Iterable<User>,
eoseCache: EOSEAccountFast<User>,
inRelays: Set<NormalizedRelayUrl>,
): Collection<List<User>> =
users
.groupBy {
eoseCache
.since(it)
?.keys
?.intersect(inRelays)
?.hashCode()
): Collection<List<User>> {
if (users.none()) return emptyList()
val relaySnapshot = inRelays.toSet()
return users
.groupBy { user ->
val relaysForUser = eoseCache.sinceRelaySet(user)
if (relaysForUser.isNullOrEmpty() || relaySnapshot.isEmpty()) {
null
} else {
val intersection = relaysForUser.filter { it in relaySnapshot }.sorted()
if (intersection.isEmpty()) {
null
} else {
intersection.hashCode()
}
}
}.values
.map {
// important to keep in order otherwise the Relay thinks the filter has changed and we REQ again
it.sortedBy { it.pubkeyHex }
}
}
fun findMinimumEOSEsForUsers(
users: List<User>,

View File

@@ -125,36 +125,51 @@ class EOSEAccountFast<T : Any>(
cacheSize: Int = 20,
) {
private val users: LruCache<T, EOSERelayList> = LruCache<T, EOSERelayList>(cacheSize)
private val lock = Any()
fun addOrUpdate(
user: T,
relayUrl: NormalizedRelayUrl,
time: Long,
) {
val relayList = users[user]
if (relayList == null) {
val newList = EOSERelayList()
users.put(user, newList)
synchronized(lock) {
val relayList = users[user]
if (relayList == null) {
val newList = EOSERelayList()
users.put(user, newList)
newList.addOrUpdate(relayUrl, time)
} else {
relayList.addOrUpdate(relayUrl, time)
newList.addOrUpdate(relayUrl, time)
} else {
relayList.addOrUpdate(relayUrl, time)
}
}
}
fun removeEveryoneBut(list: Set<T>) {
users.snapshot().forEach {
if (it.key !in list) {
users.remove(it.key)
synchronized(lock) {
users.snapshot().forEach {
if (it.key !in list) {
users.remove(it.key)
}
}
}
}
fun removeDataFor(user: T) {
users.remove(user)
synchronized(lock) {
users.remove(user)
}
}
fun since(key: T) = users[key]?.relayList
fun since(key: T): SincePerRelayMap? =
synchronized(lock) {
users[key]?.relayList?.toMutableMap()
}
fun sinceRelaySet(key: T): Set<NormalizedRelayUrl>? =
synchronized(lock) {
users[key]?.relayList?.keys?.toSet()
}
fun newEose(
user: T,

View File

@@ -35,7 +35,6 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor

View File

@@ -43,7 +43,6 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.UserComposeNoAction
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.list.PeopleListItem
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.HalfHalfHorzModifier

View File

@@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Groups
import androidx.compose.material.icons.outlined.Lock