Skip to content

Commit

Permalink
Make leaderboards scale better when there is a lot of boards and players
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikSzabo committed Jul 12, 2024
1 parent 41a7004 commit 3f72fc5
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 200 deletions.
9 changes: 6 additions & 3 deletions src/main/java/gg/auroramc/aurora/api/user/UserManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -160,16 +160,19 @@ public void loadUser(UUID uuid) {
var maybeUser = cache.getIfPresent(uuid);

if (maybeUser != null && !maybeUser.isLoaded()) {
lbm.updateUser(user).join();
lbm.updateUser(user).thenAcceptAsync(a ->
lbm.loadUser(user.getUniqueId()).thenAcceptAsync(maybeUser.getLeaderboardEntries()::putAll));

maybeUser.getLeaderboardEntries().putAll(lbm.loadUser(user.getUniqueId()).join());
maybeUser.loadFromUser(user);
Aurora.logger().debug("Updated user " + user.getUniqueId() + " in cache");

Bukkit.getGlobalRegionScheduler().run(Aurora.getInstance(),
(task) -> Bukkit.getPluginManager().callEvent(new AuroraUserLoadedEvent(user)));
} else {
lbm.updateUser(user).join();
user.getLeaderboardEntries().putAll(lbm.loadUser(user.getUniqueId()).join());
lbm.updateUser(user).thenAcceptAsync(a ->
lbm.loadUser(user.getUniqueId()).thenAcceptAsync(user.getLeaderboardEntries()::putAll));

cache.put(uuid, user);
Aurora.logger().debug("Loaded user " + user.getUniqueId() + " into cache");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import gg.auroramc.aurora.api.user.storage.SaveReason;
import gg.auroramc.aurora.api.user.storage.UserStorage;
import gg.auroramc.aurora.expansions.leaderboard.model.LbEntry;
import gg.auroramc.aurora.expansions.leaderboard.storage.BoardValue;
import gg.auroramc.aurora.expansions.leaderboard.storage.LeaderboardStorage;
import lombok.SneakyThrows;
import org.bukkit.Bukkit;
Expand Down Expand Up @@ -309,40 +310,29 @@ public List<LbEntry> getTopEntries(String board, int limit) {
}

@Override
public Map<String, LbEntry> getPlayerEntries(UUID uuid, Set<String> boards) {
public Map<String, LbEntry> getPlayerEntries(UUID uuid) {
Map<String, LbEntry> entries = new HashMap<>();

String boardNamesPlaceholders = String.join(",", Collections.nCopies(boards.size(), "?"));

String query = """
WITH RankedEntries AS (
SELECT
player_uuid,
name,
board,
value,
RANK() OVER (PARTITION BY board ORDER BY value DESC) as position
FROM aurora_leaderboard
WHERE board IN (""" + boardNamesPlaceholders + """
))
SELECT player_uuid, name, board, value, position
FROM RankedEntries
WHERE player_uuid = ?
""";
WITH RankedEntries AS (
SELECT
player_uuid,
name,
board,
value,
RANK() OVER (PARTITION BY board ORDER BY value DESC) as position
FROM aurora_leaderboard
)
SELECT player_uuid, name, board, value, position
FROM RankedEntries
WHERE player_uuid = ?
""";

try (Connection conn = connection();
PreparedStatement ps = conn.prepareStatement(query)) {

int index = 1;

// Bind board names to the prepared statement
for (String board : boards) {
ps.setString(index, board);
index++;
}

// Bind the player UUID to the prepared statement
ps.setString(index, uuid.toString());
ps.setString(1, uuid.toString());

ResultSet rs = ps.executeQuery();
while (rs.next()) {
Expand All @@ -360,58 +350,6 @@ WHERE board IN (""" + boardNamesPlaceholders + """
return entries;
}

@Override
public Map<UUID, Map<String, LbEntry>> getPlayerEntries(Collection<? extends Player> players, Set<String> boards) {
Map<UUID, Map<String, LbEntry>> entries = new HashMap<>();

String playerUuids = String.join(",", Collections.nCopies(players.size(), "?"));
String boardNames = String.join(",", Collections.nCopies(boards.size(), "?"));

String query = """
WITH RankedEntries AS (
SELECT
player_uuid,
name,
board,
value,
RANK() OVER (PARTITION BY board ORDER BY value DESC) as position
FROM aurora_leaderboard
WHERE board IN (""" + boardNames + """
))
SELECT player_uuid, name, board, value, position
FROM RankedEntries
WHERE player_uuid IN (""" + playerUuids + """
)""";

try (Connection conn = connection();
PreparedStatement ps = conn.prepareStatement(query)) {

int index = 1;
for (String board : boards) {
ps.setString(index++, board);
}
for (Player player : players) {
ps.setString(index++, player.getUniqueId().toString());
}

ResultSet rs = ps.executeQuery();
while (rs.next()) {
UUID uuid = UUID.fromString(rs.getString("player_uuid"));
String boardName = rs.getString("board");
String name = rs.getString("name");
double value = rs.getDouble("value");
long position = rs.getLong("position");

entries.computeIfAbsent(uuid, k -> new HashMap<>())
.put(boardName, new LbEntry(uuid, name, boardName, value, position));
}
} catch (SQLException e) {
e.printStackTrace();
}

return entries;
}

@Override
public long getTotalEntryCount(String board) {
String query = "SELECT COUNT(*) as total FROM " + leaderboardTableName + " WHERE board = ?";
Expand All @@ -431,30 +369,28 @@ public long getTotalEntryCount(String board) {
}

@Override
public boolean updateEntry(String board, UUID uuid, double value) {
String existsQuery = "SELECT player_uuid FROM " + leaderboardTableName + " WHERE player_uuid = ? AND board = ?";
public void updateEntry(UUID uuid, Set<BoardValue> values) {
String query = "INSERT INTO " + leaderboardTableName + " (player_uuid, name, board, value) " +
"VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE value = ?, name = ?";

try (Connection conn = connection();
PreparedStatement psCheck = conn.prepareStatement(existsQuery);
PreparedStatement ps = conn.prepareStatement(query)) {
psCheck.setString(1, uuid.toString());
psCheck.setString(2, board);
var rs = psCheck.executeQuery();

ps.setString(1, uuid.toString());
ps.setString(2, Bukkit.getOfflinePlayer(uuid).getName()); // Replace with actual player name retrieval
ps.setString(3, board);
ps.setDouble(4, value);
ps.setDouble(5, value);
ps.setString(6, Bukkit.getOfflinePlayer(uuid).getName());

boolean exists = rs.next();
ps.executeUpdate();
return !exists;
var name = Bukkit.getOfflinePlayer(uuid).getName();

for (BoardValue boardValue : values) {
ps.setString(1, uuid.toString());
ps.setString(2, name);
ps.setString(3, boardValue.board());
ps.setDouble(4, boardValue.value());
ps.setDouble(5, boardValue.value());
ps.setString(6, name);
ps.addBatch();
}

ps.executeBatch();
} catch (SQLException e) {
e.printStackTrace();
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import gg.auroramc.aurora.api.user.AuroraUser;
import gg.auroramc.aurora.api.user.storage.sql.MySqlStorage;
import gg.auroramc.aurora.expansions.leaderboard.model.LbEntry;
import gg.auroramc.aurora.expansions.leaderboard.storage.BoardValue;
import gg.auroramc.aurora.expansions.leaderboard.storage.LeaderboardStorage;
import gg.auroramc.aurora.expansions.leaderboard.storage.sqlite.SqliteLeaderboardStorage;
import org.bukkit.Bukkit;
Expand Down Expand Up @@ -56,8 +57,11 @@ public void updateTask() {
boardSizes.put(board, storage.getTotalEntryCount(board));
}

storage.getPlayerEntries(Bukkit.getOnlinePlayers(), valueMappers.keySet()).forEach((uuid, stringLbEntryMap) ->
Aurora.getUserManager().getUser(uuid).getLeaderboardEntries().putAll(stringLbEntryMap));
Bukkit.getOnlinePlayers().forEach(player -> {
if (Bukkit.isStopping() || Aurora.isDisabling()) return;
var user = Aurora.getUserManager().getUser(player.getUniqueId());
user.getLeaderboardEntries().putAll(storage.getPlayerEntries(player.getUniqueId()));
});

if (!Bukkit.isStopping() && !Aurora.isDisabling()) {
updateTask();
Expand Down Expand Up @@ -131,7 +135,7 @@ public long getBoardSize(String board) {
public CompletableFuture<Map<String, LbEntry>> loadUser(UUID uuid) {
return CompletableFuture.supplyAsync(() -> {
synchronized (getUpdateLock(uuid)) {
return storage.getPlayerEntries(uuid, valueMappers.keySet());
return storage.getPlayerEntries(uuid);
}
});
}
Expand All @@ -145,11 +149,14 @@ public CompletableFuture<Map<String, LbEntry>> loadUser(UUID uuid) {
public CompletableFuture<Void> updateUser(AuroraUser user, String... updateBoards) {
return CompletableFuture.runAsync(() -> {
synchronized (getUpdateLock(user.getUniqueId())) {
var toUpdate = new HashSet<BoardValue>(updateBoards.length == 0 ? valueMappers.keySet().size() : updateBoards.length);

for (var board : updateBoards.length == 0 ? valueMappers.keySet() : Arrays.asList(updateBoards)) {
double value = valueMappers.get(board).apply(user);
boolean isNewEntry = storage.updateEntry(board, user.getUniqueId(), value);
if(isNewEntry) boardSizes.put(board, boardSizes.get(board) + 1);
toUpdate.add(new BoardValue(board, value));
}

storage.updateEntry(user.getUniqueId(), toUpdate);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package gg.auroramc.aurora.expansions.leaderboard.storage;

public record BoardValue(String board, double value) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@

public interface LeaderboardStorage {
List<LbEntry> getTopEntries(String board, int limit);
Map<String, LbEntry> getPlayerEntries(UUID uuid, Set<String> boards);
Map<UUID, Map<String, LbEntry>> getPlayerEntries(Collection<? extends Player> player, Set<String> boards);
Map<String, LbEntry> getPlayerEntries(UUID uuid);
long getTotalEntryCount(String board);
boolean updateEntry(String board, UUID uuid, double value);
void updateEntry(UUID uuid, Set<BoardValue> values);
}
Loading

0 comments on commit 3f72fc5

Please sign in to comment.