Skip to content

Commit

Permalink
Username resolving rework (#1311)
Browse files Browse the repository at this point in the history
Signed-off-by: Pablo Herrera <[email protected]>
  • Loading branch information
Pablete1234 authored May 12, 2024
1 parent c5cbdb3 commit 6d924c8
Show file tree
Hide file tree
Showing 24 changed files with 700 additions and 272 deletions.
9 changes: 9 additions & 0 deletions core/src/main/java/tc/oc/pgm/PGMPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import tc.oc.pgm.command.util.PGMCommandGraph;
import tc.oc.pgm.db.CacheDatastore;
import tc.oc.pgm.db.SQLDatastore;
import tc.oc.pgm.db.SqlUsernameResolver;
import tc.oc.pgm.integrations.SimpleVanishIntegration;
import tc.oc.pgm.listeners.AntiGriefListener;
import tc.oc.pgm.listeners.BlockTransformListener;
Expand Down Expand Up @@ -81,6 +82,9 @@
import tc.oc.pgm.util.tablist.TablistResizer;
import tc.oc.pgm.util.text.TextException;
import tc.oc.pgm.util.text.TextTranslations;
import tc.oc.pgm.util.usernames.ApiUsernameResolver;
import tc.oc.pgm.util.usernames.BukkitUsernameResolver;
import tc.oc.pgm.util.usernames.UsernameResolvers;
import tc.oc.pgm.util.xml.InvalidXMLException;

public class PGMPlugin extends JavaPlugin implements PGM, Listener {
Expand Down Expand Up @@ -162,6 +166,11 @@ public void onEnable() {
return;
}

UsernameResolvers.setResolvers(
new BukkitUsernameResolver(),
new SqlUsernameResolver((SQLDatastore) datastore),
new ApiUsernameResolver());

datastore = new CacheDatastore(datastore);

try {
Expand Down
7 changes: 0 additions & 7 deletions core/src/main/java/tc/oc/pgm/api/player/Username.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,4 @@ public interface Username extends Named {
@Override
@Nullable
String getNameLegacy();

/**
* Change the username of the player.
*
* @param name The new username or null.
*/
void setName(@Nullable String name);
}
84 changes: 39 additions & 45 deletions core/src/main/java/tc/oc/pgm/db/SQLDatastore.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package tc.oc.pgm.db;

import static tc.oc.pgm.util.Assert.assertNotNull;
import static tc.oc.pgm.util.player.PlayerComponent.player;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Duration;
import java.time.Instant;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import net.kyori.adventure.text.Component;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.Nullable;
import tc.oc.pgm.api.Datastore;
import tc.oc.pgm.api.map.MapActivity;
Expand All @@ -14,8 +20,11 @@
import tc.oc.pgm.api.setting.SettingValue;
import tc.oc.pgm.api.setting.Settings;
import tc.oc.pgm.util.concurrent.ThreadSafeConnection;
import tc.oc.pgm.util.named.NameStyle;
import tc.oc.pgm.util.skin.Skin;
import tc.oc.pgm.util.text.TextParser;
import tc.oc.pgm.util.usernames.UsernameResolver;
import tc.oc.pgm.util.usernames.UsernameResolvers;

public class SQLDatastore extends ThreadSafeConnection implements Datastore {

Expand All @@ -31,54 +40,44 @@ public SQLDatastore(String uri, int maxConnections) throws SQLException {
"CREATE TABLE IF NOT EXISTS pools (name VARCHAR(255) PRIMARY KEY, next_map VARCHAR(255), last_active BOOLEAN)");
}

private class SQLUsername extends UsernameImpl {
private class SQLUsername implements Username {
private final Duration ONE_WEEK = Duration.ofDays(7);

private volatile boolean queried;
private final UUID id;
private String name;
private Instant validUntil;

SQLUsername(UUID id, @Nullable String name) {
super(id, name);
SQLUsername(UUID id) {
this.id = assertNotNull(id, "username id is null");
UsernameResolvers.resolve(id).thenAccept(this::setName);
}

@Override
public String getNameLegacy() {
String name = super.getNameLegacy();

// Since there can be hundreds of names, only query when requested.
if (!queried && name == null) {
queried = true;
submitQuery(new SelectQuery());
}
public UUID getId() {
return id;
}

@Override
public String getNameLegacy() {
return name;
}

@Override
public void setName(@Nullable String name) {
super.setName(name);

if (name != null) {
submitQuery(new UpdateQuery());
}
public Component getName(NameStyle style) {
return player(Bukkit.getPlayer(id), name, style);
}

private class SelectQuery implements Query {
@Override
public String getFormat() {
return "SELECT name, expires FROM usernames WHERE id = ? LIMIT 1";
}

@Override
public void query(PreparedStatement statement) throws SQLException {
statement.setString(1, getId().toString());

try (final ResultSet result = statement.executeQuery()) {
if (!result.next()) return;

setName(result.getString(1));

if (result.getLong(2) < System.currentTimeMillis()) {
setName(null);
}
protected void setName(UsernameResolver.UsernameResponse response) {
// A name is provided and either we know no name, or it's more recent
if (response.getUsername() != null
&& (this.name == null || response.getValidUntil().isAfter(this.validUntil))) {
this.name = response.getUsername();
this.validUntil = response.getValidUntil();

// Only update names with about over a week of validity
if (response.getSource() != SqlUsernameResolver.class
&& validUntil.isAfter(Instant.now().plus(ONE_WEEK))) {
submitQuery(new UpdateQuery());
}
}
}
Expand All @@ -91,22 +90,17 @@ public String getFormat() {

@Override
public void query(PreparedStatement statement) throws SQLException {
statement.setString(1, getId().toString());
statement.setString(2, getNameLegacy());

// Pick a random expiration time between 1 and 2 weeks
statement.setLong(
3,
System.currentTimeMillis() + Duration.ofDays(7 + (int) (Math.random() * 7)).toMillis());

statement.setString(1, id.toString());
statement.setString(2, name);
statement.setLong(3, validUntil.toEpochMilli());
statement.executeUpdate();
}
}
}

@Override
public Username getUsername(UUID id) {
return new SQLUsername(id, null);
return new SQLUsername(id);
}

private class SQLSettings extends SettingsImpl {
Expand Down
113 changes: 113 additions & 0 deletions core/src/main/java/tc/oc/pgm/db/SqlUsernameResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package tc.oc.pgm.db;

import com.google.common.collect.Lists;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import tc.oc.pgm.util.concurrent.ThreadSafeConnection;
import tc.oc.pgm.util.usernames.AbstractBatchingUsernameResolver;

public class SqlUsernameResolver extends AbstractBatchingUsernameResolver {
private static final int BATCH_SIZE = 500;
private final SQLDatastore datastore;

public SqlUsernameResolver(SQLDatastore datastore) {
this.datastore = datastore;
}

protected void process(UUID uuid, CompletableFuture<UsernameResponse> future) {
datastore.submitQuery(new SingleSelect(uuid, future)).join();
}

@Override
protected void process(List<UUID> uuids) {
List<List<UUID>> partitions = Lists.partition(uuids, BATCH_SIZE);
CompletableFuture<?>[] futures = new CompletableFuture[partitions.size()];
for (int i = 0; i < partitions.size(); i++) {
futures[i] = datastore.submitQuery(new BatchSelect(partitions.get(i), this::complete));
}
CompletableFuture.allOf(futures).join();
}

private static class SingleSelect implements ThreadSafeConnection.Query {
private final UUID uuid;
private final CompletableFuture<UsernameResponse> future;

public SingleSelect(UUID uuid, CompletableFuture<UsernameResponse> future) {
this.uuid = uuid;
this.future = future;
}

@Override
public String getFormat() {
return "SELECT name, expires FROM usernames WHERE id = ? LIMIT 1";
}

@Override
public void query(PreparedStatement statement) throws SQLException {
statement.setString(1, uuid.toString());

try (final ResultSet result = statement.executeQuery()) {
future.complete(
!result.next()
? UsernameResponse.empty()
: UsernameResponse.of(
result.getString(1),
null,
Instant.ofEpochMilli(result.getLong(2)),
SqlUsernameResolver.class));
}
}
}

private static class BatchSelect implements ThreadSafeConnection.Query {
private final List<UUID> uuids;
private final BiConsumer<UUID, UsernameResponse> completion;

public BatchSelect(List<UUID> uuids, BiConsumer<UUID, UsernameResponse> completion) {
this.uuids = uuids;
this.completion = completion;
}

@Override
public String getFormat() {
return "SELECT id, name, expires FROM usernames WHERE id IN ("
+ Stream.generate(() -> "?").limit(uuids.size()).collect(Collectors.joining(","))
+ ")";
}

@Override
public void query(PreparedStatement statement) throws SQLException {
for (int i = 0; i < uuids.size(); i++) {
statement.setString(i + 1, uuids.get(i).toString());
}

try (final ResultSet result = statement.executeQuery()) {
Set<UUID> leftover = new HashSet<>(uuids);
while (result.next()) {
UUID uuid = UUID.fromString(result.getString(1));
leftover.remove(uuid);

completion.accept(
uuid,
UsernameResponse.of(
result.getString(2),
null,
Instant.ofEpochMilli(result.getLong(3)),
SqlUsernameResolver.class));
}

leftover.forEach(uuid -> completion.accept(uuid, UsernameResponse.empty()));
}
}
}
}
65 changes: 0 additions & 65 deletions core/src/main/java/tc/oc/pgm/db/UsernameImpl.java

This file was deleted.

7 changes: 4 additions & 3 deletions core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import tc.oc.pgm.api.map.includes.MapIncludeProcessor;
import tc.oc.pgm.util.LiquidMetal;
import tc.oc.pgm.util.StringUtils;
import tc.oc.pgm.util.UsernameResolver;
import tc.oc.pgm.util.usernames.UsernameResolvers;

public class MapLibraryImpl implements MapLibrary {

Expand Down Expand Up @@ -129,7 +129,8 @@ public CompletableFuture<?> loadNewMaps(boolean reset) {
final int oldFail = failed.size();
final int oldOk = reset ? 0 : maps.size();

return CompletableFuture.runAsync(
return CompletableFuture.runAsync(UsernameResolvers::startBatch)
.thenRunAsync(
() -> {
// First ensure loadNewSources is called for all factories, this may take some time
// (eg: Git pull)
Expand Down Expand Up @@ -168,7 +169,7 @@ public CompletableFuture<?> loadNewMaps(boolean reset) {
}
})
.thenRunAsync(() -> logMapSuccess(oldFail, oldOk))
.thenRunAsync(UsernameResolver::resolveAll);
.thenRunAsync(UsernameResolvers::endBatch);
}

@Override
Expand Down
Loading

0 comments on commit 6d924c8

Please sign in to comment.