Restore BauweltMember cache and notify all dev servers on member changes

getBauMember never read its cache after the Exposed migration, so every
scoreboard render hit the DB (Too many connections, #479). Cache by
(bauwelt, member) incl. negative results, evict on addMember/remove, and
send BaumemberUpdatePacket to the Bauserver and all dev servers (also on
addmember). BauMemberUpdate clears the cache itself to not depend on
handler order.
This commit is contained in:
2026-10-19 10:24:41 +02:00
parent 27d260a8d2
commit 0c3e63d521
3 changed files with 43 additions and 18 deletions
@@ -28,6 +28,7 @@ import de.steamwar.bausystem.utils.bossbar.BossBarService;
import de.steamwar.linkage.Linked;
import de.steamwar.network.packets.PacketHandler;
import de.steamwar.network.packets.server.BaumemberUpdatePacket;
import de.steamwar.sql.BauweltMember;
import io.papermc.paper.event.player.PlayerPickBlockEvent;
import org.bukkit.Bukkit;
import org.bukkit.boss.BarColor;
@@ -57,6 +58,8 @@ public class BauMemberUpdate extends PacketHandler implements Listener {
}
public static void baumemberUpdate() {
// Packet handlers run in no guaranteed order, so don't rely on CoreNetworkHandler having cleared the cache yet.
BauweltMember.clear();
Set<Player> newSpectator = new HashSet<>();
Set<Player> newBuilder = new HashSet<>();
Bukkit.getOnlinePlayers().forEach(player -> {
@@ -30,6 +30,7 @@ import org.jetbrains.exposed.v1.dao.CompositeEntity
import org.jetbrains.exposed.v1.dao.CompositeEntityClass
import org.jetbrains.exposed.v1.jdbc.insertIgnore
import java.util.*
import java.util.concurrent.ConcurrentHashMap
object BauweltMemberTable : CompositeIdTable("BauweltMember") {
val bauweltId = reference("BauweltID", SteamwarUserTable).index()
@@ -48,10 +49,12 @@ object BauweltMemberTable : CompositeIdTable("BauweltMember") {
class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
companion object : CompositeEntityClass<BauweltMember>(BauweltMemberTable) {
private val cache = mutableMapOf<Int, BauweltMember>()
// Keyed by (bauweltId, memberId). Optional.empty() caches "is not a member", which is the
// common case for spectators and would otherwise hit the DB on every permission check.
private val cache = ConcurrentHashMap<Pair<Int, Int>, Optional<BauweltMember>>()
private fun cache(member: BauweltMember) =
cache.put(member.memberID, member)
cache.put(member.bauweltID to member.memberID, Optional.of(member))
@JvmStatic
fun clear() =
@@ -75,20 +78,20 @@ class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
it[worldEdit] = false
it[world] = false
}
}
}.also { cache.remove(ownerId.value to newMemberId.value) }
@JvmStatic
@Deprecated("Use getBauMember(bauwelt: Int, member: Int)")
fun getBauMember(bauwelt: UUID, member: UUID) =
useDb {
find { (bauweltId eq SteamwarUser.get(bauwelt)!!.id) and (BauweltMemberTable.memberId eq SteamwarUser.get(member)!!.id) }.firstOrNull()?.also { cache(it) }
}
getBauMember(SteamwarUser.get(bauwelt)!!.id.value, SteamwarUser.get(member)!!.id.value)
@JvmStatic
fun getBauMember(bauwelt: Int, member: Int) =
useDb {
find { (bauweltId eq bauwelt) and (BauweltMemberTable.memberId eq member) }.firstOrNull()?.also { cache(it) }
}
fun getBauMember(bauwelt: Int, member: Int): BauweltMember? =
cache.computeIfAbsent(bauwelt to member) {
Optional.ofNullable(useDb {
find { (bauweltId eq bauwelt) and (BauweltMemberTable.memberId eq member) }.firstOrNull()
})
}.orElse(null)
@JvmStatic
@Deprecated("Use getMembers(bauwelt: Int)")
@@ -140,8 +143,11 @@ class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
fun isWorld() =
world
fun remove() =
fun remove() {
val key = bauweltID to memberID
useDb {
delete()
}
cache.remove(key)
}
}
@@ -19,6 +19,7 @@
package de.steamwar.velocitycore.commands;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import de.steamwar.command.PreviousArguments;
import de.steamwar.command.SWCommand;
import de.steamwar.command.TypeMapper;
@@ -45,7 +46,9 @@ import de.steamwar.velocitycore.network.NetworkSender;
import de.steamwar.velocitycore.util.BauLock;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.UUID;
import java.util.function.Consumer;
@Linked
@@ -72,6 +75,7 @@ public class BauCommand extends SWCommand {
@Register(value = "addmember", description = "BAU_ADDMEMBER_USAGE")
public void addmember(Chatter sender, @Validator("addMemberTarget") SteamwarUser target) {
BauweltMember.addMember(sender.user().getUUID(), target.getUUID());
sendBaumemberUpdate(sender.user().getUUID());
sender.system("BAU_ADDMEMBER_ADDED");
Chatter.of(target.getUUID()).system("BAU_ADDMEMBER_ADDED_TARGET", sender);
}
@@ -161,10 +165,7 @@ public class BauCommand extends SWCommand {
withMember(owner, user, target -> {
setter.accept(target);
Bauserver bauserver = Bauserver.get(owner.user().getUUID());
if (bauserver != null) {
bauserver.getRegisteredServer().getPlayersConnected().stream().findAny().ifPresent(player -> NetworkSender.send(player, new BaumemberUpdatePacket()));
}
sendBaumemberUpdate(owner.user().getUUID());
Chatter.of(user.getUUID()).system("BAU_MEMBER_SET_TARGET", owner, new Message(permName));
owner.system("BAU_MEMBER_SET", new Message(permName));
@@ -184,9 +185,7 @@ public class BauCommand extends SWCommand {
}
});
if (bauserver != null) {
bauserver.getRegisteredServer().getPlayersConnected().stream().findAny().ifPresent(player -> NetworkSender.send(player, new BaumemberUpdatePacket()));
}
sendBaumemberUpdate(owner.user().getUUID());
try {
deletePlayerData(owner, user);
@@ -289,6 +288,23 @@ public class BauCommand extends SWCommand {
BauLock.setLocked(sender, BauLockState.OPEN);
}
/**
* Tells the owner's Bauserver and every dev server to drop their cached {@link BauweltMember}s,
* since those run in separate processes and would otherwise keep serving stale permissions.
* Plugin messages need a connected player, so servers without players are skipped.
*/
private static void sendBaumemberUpdate(UUID owner) {
Bauserver bauserver = Bauserver.get(owner);
if (bauserver != null) {
sendBaumemberUpdate(bauserver.getRegisteredServer());
}
new ArrayList<>(DevCommand.getDevServers().values()).forEach(BauCommand::sendBaumemberUpdate);
}
private static void sendBaumemberUpdate(RegisteredServer server) {
server.getPlayersConnected().stream().findAny().ifPresent(player -> NetworkSender.send(player, new BaumemberUpdatePacket()));
}
private static void withMember(Chatter owner, SteamwarUser member, Consumer<BauweltMember> function) {
if (member == null) {
owner.system("UNKNOWN_PLAYER");