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.linkage.Linked;
import de.steamwar.network.packets.PacketHandler; import de.steamwar.network.packets.PacketHandler;
import de.steamwar.network.packets.server.BaumemberUpdatePacket; import de.steamwar.network.packets.server.BaumemberUpdatePacket;
import de.steamwar.sql.BauweltMember;
import io.papermc.paper.event.player.PlayerPickBlockEvent; import io.papermc.paper.event.player.PlayerPickBlockEvent;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.boss.BarColor; import org.bukkit.boss.BarColor;
@@ -57,6 +58,8 @@ public class BauMemberUpdate extends PacketHandler implements Listener {
} }
public static void baumemberUpdate() { 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> newSpectator = new HashSet<>();
Set<Player> newBuilder = new HashSet<>(); Set<Player> newBuilder = new HashSet<>();
Bukkit.getOnlinePlayers().forEach(player -> { 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.dao.CompositeEntityClass
import org.jetbrains.exposed.v1.jdbc.insertIgnore import org.jetbrains.exposed.v1.jdbc.insertIgnore
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap
object BauweltMemberTable : CompositeIdTable("BauweltMember") { object BauweltMemberTable : CompositeIdTable("BauweltMember") {
val bauweltId = reference("BauweltID", SteamwarUserTable).index() val bauweltId = reference("BauweltID", SteamwarUserTable).index()
@@ -48,10 +49,12 @@ object BauweltMemberTable : CompositeIdTable("BauweltMember") {
class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) { class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
companion object : CompositeEntityClass<BauweltMember>(BauweltMemberTable) { 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) = private fun cache(member: BauweltMember) =
cache.put(member.memberID, member) cache.put(member.bauweltID to member.memberID, Optional.of(member))
@JvmStatic @JvmStatic
fun clear() = fun clear() =
@@ -75,20 +78,20 @@ class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
it[worldEdit] = false it[worldEdit] = false
it[world] = false it[world] = false
} }
} }.also { cache.remove(ownerId.value to newMemberId.value) }
@JvmStatic @JvmStatic
@Deprecated("Use getBauMember(bauwelt: Int, member: Int)") @Deprecated("Use getBauMember(bauwelt: Int, member: Int)")
fun getBauMember(bauwelt: UUID, member: UUID) = fun getBauMember(bauwelt: UUID, member: UUID) =
useDb { getBauMember(SteamwarUser.get(bauwelt)!!.id.value, SteamwarUser.get(member)!!.id.value)
find { (bauweltId eq SteamwarUser.get(bauwelt)!!.id) and (BauweltMemberTable.memberId eq SteamwarUser.get(member)!!.id) }.firstOrNull()?.also { cache(it) }
}
@JvmStatic @JvmStatic
fun getBauMember(bauwelt: Int, member: Int) = fun getBauMember(bauwelt: Int, member: Int): BauweltMember? =
useDb { cache.computeIfAbsent(bauwelt to member) {
find { (bauweltId eq bauwelt) and (BauweltMemberTable.memberId eq member) }.firstOrNull()?.also { cache(it) } Optional.ofNullable(useDb {
} find { (bauweltId eq bauwelt) and (BauweltMemberTable.memberId eq member) }.firstOrNull()
})
}.orElse(null)
@JvmStatic @JvmStatic
@Deprecated("Use getMembers(bauwelt: Int)") @Deprecated("Use getMembers(bauwelt: Int)")
@@ -140,8 +143,11 @@ class BauweltMember(id: EntityID<CompositeID>) : CompositeEntity(id) {
fun isWorld() = fun isWorld() =
world world
fun remove() = fun remove() {
val key = bauweltID to memberID
useDb { useDb {
delete() delete()
} }
cache.remove(key)
}
} }
@@ -19,6 +19,7 @@
package de.steamwar.velocitycore.commands; package de.steamwar.velocitycore.commands;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import de.steamwar.command.PreviousArguments; import de.steamwar.command.PreviousArguments;
import de.steamwar.command.SWCommand; import de.steamwar.command.SWCommand;
import de.steamwar.command.TypeMapper; import de.steamwar.command.TypeMapper;
@@ -45,7 +46,9 @@ import de.steamwar.velocitycore.network.NetworkSender;
import de.steamwar.velocitycore.util.BauLock; import de.steamwar.velocitycore.util.BauLock;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.UUID;
import java.util.function.Consumer; import java.util.function.Consumer;
@Linked @Linked
@@ -72,6 +75,7 @@ public class BauCommand extends SWCommand {
@Register(value = "addmember", description = "BAU_ADDMEMBER_USAGE") @Register(value = "addmember", description = "BAU_ADDMEMBER_USAGE")
public void addmember(Chatter sender, @Validator("addMemberTarget") SteamwarUser target) { public void addmember(Chatter sender, @Validator("addMemberTarget") SteamwarUser target) {
BauweltMember.addMember(sender.user().getUUID(), target.getUUID()); BauweltMember.addMember(sender.user().getUUID(), target.getUUID());
sendBaumemberUpdate(sender.user().getUUID());
sender.system("BAU_ADDMEMBER_ADDED"); sender.system("BAU_ADDMEMBER_ADDED");
Chatter.of(target.getUUID()).system("BAU_ADDMEMBER_ADDED_TARGET", sender); Chatter.of(target.getUUID()).system("BAU_ADDMEMBER_ADDED_TARGET", sender);
} }
@@ -161,10 +165,7 @@ public class BauCommand extends SWCommand {
withMember(owner, user, target -> { withMember(owner, user, target -> {
setter.accept(target); setter.accept(target);
Bauserver bauserver = Bauserver.get(owner.user().getUUID()); sendBaumemberUpdate(owner.user().getUUID());
if (bauserver != null) {
bauserver.getRegisteredServer().getPlayersConnected().stream().findAny().ifPresent(player -> NetworkSender.send(player, new BaumemberUpdatePacket()));
}
Chatter.of(user.getUUID()).system("BAU_MEMBER_SET_TARGET", owner, new Message(permName)); Chatter.of(user.getUUID()).system("BAU_MEMBER_SET_TARGET", owner, new Message(permName));
owner.system("BAU_MEMBER_SET", new Message(permName)); owner.system("BAU_MEMBER_SET", new Message(permName));
@@ -184,9 +185,7 @@ public class BauCommand extends SWCommand {
} }
}); });
if (bauserver != null) { sendBaumemberUpdate(owner.user().getUUID());
bauserver.getRegisteredServer().getPlayersConnected().stream().findAny().ifPresent(player -> NetworkSender.send(player, new BaumemberUpdatePacket()));
}
try { try {
deletePlayerData(owner, user); deletePlayerData(owner, user);
@@ -289,6 +288,23 @@ public class BauCommand extends SWCommand {
BauLock.setLocked(sender, BauLockState.OPEN); 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) { private static void withMember(Chatter owner, SteamwarUser member, Consumer<BauweltMember> function) {
if (member == null) { if (member == null) {
owner.system("UNKNOWN_PLAYER"); owner.system("UNKNOWN_PLAYER");