* '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.stateIn
import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlin.collections.map
/** /**
* Maintains several stateflows for each step in processing PeopleLists * 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. // removes accounts that are not being subscribed anymore.
accountUpdatesJobMap.forEach { // Cancel watchers for accounts no longer observed using a snapshot to avoid CME
if (it.key !in uniqueSubscribedAccounts.keys) { accountUpdatesJobMap.keys
endWatcher(it.key) .toList()
} .filter { it !in uniqueSubscribedAccounts.keys }
} .forEach { endWatcher(it) }
} }
private val accountUpdatesJobMap = mutableMapOf<User, Job>() private val accountUpdatesJobMap = mutableMapOf<User, Job>()

View File

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

View File

@@ -125,36 +125,51 @@ class EOSEAccountFast<T : Any>(
cacheSize: Int = 20, cacheSize: Int = 20,
) { ) {
private val users: LruCache<T, EOSERelayList> = LruCache<T, EOSERelayList>(cacheSize) private val users: LruCache<T, EOSERelayList> = LruCache<T, EOSERelayList>(cacheSize)
private val lock = Any()
fun addOrUpdate( fun addOrUpdate(
user: T, user: T,
relayUrl: NormalizedRelayUrl, relayUrl: NormalizedRelayUrl,
time: Long, time: Long,
) { ) {
val relayList = users[user] synchronized(lock) {
if (relayList == null) { val relayList = users[user]
val newList = EOSERelayList() if (relayList == null) {
users.put(user, newList) val newList = EOSERelayList()
users.put(user, newList)
newList.addOrUpdate(relayUrl, time) newList.addOrUpdate(relayUrl, time)
} else { } else {
relayList.addOrUpdate(relayUrl, time) relayList.addOrUpdate(relayUrl, time)
}
} }
} }
fun removeEveryoneBut(list: Set<T>) { fun removeEveryoneBut(list: Set<T>) {
users.snapshot().forEach { synchronized(lock) {
if (it.key !in list) { users.snapshot().forEach {
users.remove(it.key) if (it.key !in list) {
users.remove(it.key)
}
} }
} }
} }
fun removeDataFor(user: T) { 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( fun newEose(
user: T, 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.account.observeAccountIsHiddenUser
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing
import com.vitorpamplona.amethyst.ui.layouts.listItem.SlimListItem 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.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor 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.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.UserComposeNoAction import com.vitorpamplona.amethyst.ui.note.UserComposeNoAction
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.HalfHalfHorzModifier 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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Groups import androidx.compose.material.icons.outlined.Groups
import androidx.compose.material.icons.outlined.Lock import androidx.compose.material.icons.outlined.Lock