Skip to content

Commit 5a7bf6a

Browse files
committed
Harden async identity and shutdown retirement
1 parent 0633af1 commit 5a7bf6a

11 files changed

Lines changed: 310 additions & 80 deletions

File tree

‎AdvancedCore/src/main/java/com/bencodez/advancedcore/api/bedrock/BedrockNameResolver.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ public boolean isBedrockName(String name) {
161161
public Result resolve(String incomingName) {
162162
if (incomingName == null || incomingName.isEmpty())
163163
return new Result(incomingName, false, "empty-name");
164+
// This synchronous compatibility API cannot wait for the shared storage
165+
// worker. On Bukkit's primary thread it therefore uses only online/cache
166+
// evidence; callers that need persisted identity must resolve off-thread.
167+
if (userManager.getDataManager() != null && userManager.getDataManager().mustDeferSharedStorageAccess()) {
168+
return resolveWithoutDb(incomingName);
169+
}
164170

165171
// An exact online identity is authoritative.
166172
Player match = findOnlineExact(incomingName);

‎AdvancedCore/src/main/java/com/bencodez/advancedcore/api/player/UuidLookup.java‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,10 @@ public String getUUID(String playerName) {
6868

6969
/**
7070
* Resolve a UUID using only an already-known UUID, offline-mode derivation,
71-
* an online player, the local cache, or Bukkit's OfflinePlayer fallback.
72-
* Shared-storage callers use this while running on Bukkit's primary thread so
73-
* creating a user cannot start an unadmitted native SQL lookup.
71+
* an online player, or the local cache. Shared-storage callers use this while
72+
* running on Bukkit's primary thread. In particular, this intentionally does
73+
* not use Bukkit's OfflinePlayer fallback: a name lookup there can block on a
74+
* profile/UUID lookup.
7475
*
7576
* @param playerName player name or UUID string
7677
* @return UUID string, or "" if no non-storage lookup succeeds
@@ -135,6 +136,11 @@ private String resolveUUID(String playerName, boolean allowStorageLookup) {
135136
}
136137
}
137138

139+
// This fallback can perform a blocking native/profile lookup. It is retained
140+
// for the established public getUUID API, but is never available to the
141+
// primary-thread-safe lookup above.
142+
if (!allowStorageLookup) return "";
143+
138144
// Bukkit OfflinePlayer fallback (best-effort)
139145
try {
140146
@SuppressWarnings("deprecation")

‎AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/AdvancedCoreUser.java‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,9 +516,14 @@ public AdvancedCoreUser(AdvancedCorePlugin plugin, String playerName) {
516516
loadData();
517517
UserManager users = plugin.getUserManager();
518518
UserDataManager manager = users == null ? null : users.getDataManager();
519-
uuid = manager != null && manager.mustDeferSharedStorageAccess()
519+
boolean deferredPrimaryLookup = manager != null && manager.mustDeferSharedStorageAccess();
520+
uuid = deferredPrimaryLookup
520521
? com.bencodez.advancedcore.api.player.UuidLookup.getInstance().getUUIDWithoutStorage(playerName)
521522
: PlayerManager.getInstance().getUUID(playerName);
523+
if (deferredPrimaryLookup && (uuid == null || uuid.isBlank())) {
524+
throw new IllegalStateException("Cannot synchronously construct an unresolved user on the primary thread; "
525+
+ "use UserManager.getUserAsync");
526+
}
522527
setPlayerName(playerName);
523528
}
524529

‎AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/UserManager.java‎

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -317,12 +317,68 @@ public AdvancedCoreUser getUser(Player player) {
317317
* @param playerName the player name
318318
* @return the user
319319
*/
320-
@SuppressWarnings("deprecation")
321-
public AdvancedCoreUser getUser(String playerName) {
320+
@SuppressWarnings("deprecation")
321+
public AdvancedCoreUser getUser(String playerName) {
322322
// keep behavior: AdvancedCoreUser handles name->uuid internally (but you can
323323
// refactor later)
324-
return new AdvancedCoreUser(plugin, getProperName(playerName));
325-
}
324+
return new AdvancedCoreUser(plugin, getProperName(playerName));
325+
}
326+
327+
/**
328+
* Resolve a name and deliver a UUID-backed user without blocking Bukkit's
329+
* primary thread. The fast identity sources remain synchronous; an unknown
330+
* online-mode name is resolved on the shared-storage worker and delivered on
331+
* the platform scheduler.
332+
*
333+
* <p>Callers that may run on the primary thread should use this instead of
334+
* {@link #getUser(String)} when they need to act on an uncached name.</p>
335+
*/
336+
public void getUserAsync(String playerName, Consumer<AdvancedCoreUser> success, Consumer<Throwable> failure) {
337+
if (playerName == null || playerName.trim().isEmpty()) {
338+
failure.accept(new IllegalArgumentException("Player name cannot be blank"));
339+
return;
340+
}
341+
if (dataManager == null || !dataManager.mustDeferSharedStorageAccess()) {
342+
try {
343+
success.accept(getUser(playerName));
344+
} catch (RuntimeException failureReason) {
345+
failure.accept(failureReason);
346+
}
347+
return;
348+
}
349+
350+
String resolved = UuidLookup.getInstance().getUUIDWithoutStorage(playerName);
351+
if (resolved != null && !resolved.isEmpty()) {
352+
deliverResolvedUser(resolved, getProperName(playerName), success, failure);
353+
return;
354+
}
355+
try {
356+
if (!dataManager.deferSharedStorageResult(() -> UuidLookup.getInstance().getUUID(playerName),
357+
uuid -> deliverResolvedUser(uuid, playerName, success, failure), failure)) {
358+
// The shared backend was retired between the eligibility check and
359+
// admission. Do not fall back to a synchronous profile/storage lookup on
360+
// the primary thread during that shutdown race.
361+
failure.accept(new IllegalStateException("User storage is no longer available"));
362+
}
363+
} catch (RuntimeException failureReason) {
364+
// Rejection during disable is an asynchronous resolution failure, not an
365+
// exception that should escape this callback-based API.
366+
failure.accept(failureReason);
367+
}
368+
}
369+
370+
private void deliverResolvedUser(String uuid, String playerName, Consumer<AdvancedCoreUser> success,
371+
Consumer<Throwable> failure) {
372+
try {
373+
if (uuid == null || uuid.isBlank()) {
374+
failure.accept(new IllegalArgumentException("Unable to resolve UUID for " + playerName));
375+
return;
376+
}
377+
success.accept(getUser(UUID.fromString(uuid), playerName));
378+
} catch (RuntimeException failureReason) {
379+
failure.accept(failureReason);
380+
}
381+
}
326382

327383
/**
328384
* Gets the user.

‎AdvancedCore/src/main/java/com/bencodez/advancedcore/bukkit/runtime/BukkitRuntimePlatform.java‎

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,10 @@ private void closeUserStorageAfterSharedRetirement() {
9191
return;
9292
}
9393
UserManager users = plugin.getLoadedUserManager();
94-
var mysql = plugin.getMysql();
95-
boolean ownsMysql;
96-
if (users != null && users.getDataManager().hasSharedSqlBackend()) {
97-
ownsMysql = users.getDataManager().usesSharedSqlStorage(UserStorage.MYSQL);
98-
} else {
99-
ownsMysql = plugin.getOptions() != null
100-
&& UserStorage.MYSQL.equals(plugin.getOptions().getStorageType());
101-
}
102-
Runnable closeMysql = () -> {
103-
try { if (ownsMysql && mysql != null) mysql.close(); }
104-
finally { plugin.closePendingNativeUserStorageOwners(); }
105-
};
94+
// Resolve this only after shared retirement. A successful replacement may
95+
// install a new owner while shutdown is waiting for its worker, and closing
96+
// the owner observed before that wait leaks the installed MySQL provider.
97+
Runnable closeMysql = () -> closeCurrentUserStorageOwner(users);
10698
if (users == null) {
10799
closeMysql.run();
108100
userStorageRetirement = CompletableFuture.completedFuture(null);
@@ -114,4 +106,18 @@ private void closeUserStorageAfterSharedRetirement() {
114106
userStorageRetirement = CompletableFuture.completedFuture(null);
115107
} else userStorageRetirement = retirement;
116108
}
109+
110+
private void closeCurrentUserStorageOwner(UserManager users) {
111+
try {
112+
AdvancedCorePlugin.UserStorageOwner owner = plugin.getNativeUserStorageOwner();
113+
if (owner != null) {
114+
if (owner.storageType() == UserStorage.MYSQL && owner.mysql() != null) owner.mysql().close();
115+
return;
116+
}
117+
boolean ownsMysql = users != null && users.getDataManager().hasSharedSqlBackend()
118+
? users.getDataManager().usesSharedSqlStorage(UserStorage.MYSQL)
119+
: plugin.getOptions() != null && UserStorage.MYSQL.equals(plugin.getOptions().getStorageType());
120+
if (ownsMysql && plugin.getMysql() != null) plugin.getMysql().close();
121+
} finally { plugin.closePendingNativeUserStorageOwners(); }
122+
}
117123
}

‎AdvancedCore/src/main/java/com/bencodez/advancedcore/command/CommandLoader.java‎

Lines changed: 70 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import java.util.ArrayList;
44
import java.util.Map.Entry;
5-
import java.util.UUID;
5+
import java.util.UUID;
6+
import java.util.function.Consumer;
67

78
import org.bukkit.Bukkit;
89
import org.bukkit.command.CommandSender;
@@ -51,9 +52,16 @@ public static CommandLoader getInstance() {
5152
return instance;
5253
}
5354

54-
private static boolean isBlank(String s) {
55-
return s == null || s.trim().isEmpty() || "null".equalsIgnoreCase(s.trim());
56-
}
55+
private static boolean isBlank(String s) {
56+
return s == null || s.trim().isEmpty() || "null".equalsIgnoreCase(s.trim());
57+
}
58+
59+
private void withResolvedUser(CommandSender sender, String playerName, Consumer<AdvancedCoreUser> action) {
60+
plugin.getUserManager().getUserAsync(playerName,
61+
user -> runRecipientCallback(UUID.fromString(user.getUUID()), () -> action.accept(user)),
62+
failure -> runCommandCallback(sender,
63+
() -> sender.sendMessage(MessageAPI.colorize("&cUnable to resolve UUID for " + playerName))));
64+
}
5765

5866
private ArrayList<String> perms = new ArrayList<>();
5967

@@ -208,9 +216,11 @@ public void execute(CommandSender sender, String[] args) {
208216

209217
@Override
210218
public void execute(CommandSender sender, String[] args) {
211-
AdvancedCoreUser user = plugin.getUserManager().getUser(args[1]);
212-
plugin.getRewardHandler().giveReward(user, args[2], new RewardOptions().setOnline(user.isOnline()));
213-
sendMessage(sender, "&cGave " + args[1] + " the reward file " + args[2]);
219+
withResolvedUser(sender, args[1], user -> {
220+
plugin.getRewardHandler().giveReward(user, args[2], new RewardOptions().setOnline(user.isOnline()));
221+
runCommandCallback(sender,
222+
() -> sendMessage(sender, "&cGave " + args[1] + " the reward file " + args[2]));
223+
});
214224
}
215225
});
216226

@@ -231,9 +241,10 @@ public void executeAll(CommandSender sender, String[] args) {
231241

232242
@Override
233243
public void executeSinglePlayer(CommandSender sender, String[] args) {
234-
AdvancedCoreUser user = plugin.getUserManager().getUser(args[1]);
235-
plugin.getRewardHandler().giveReward(user, args[3], new RewardOptions().setOnline(user.isOnline()));
236-
sender.sendMessage("&cGave " + args[1] + " the reward file " + args[3]);
244+
withResolvedUser(sender, args[1], user -> {
245+
plugin.getRewardHandler().giveReward(user, args[3], new RewardOptions().setOnline(user.isOnline()));
246+
runCommandCallback(sender, () -> sender.sendMessage("&cGave " + args[1] + " the reward file " + args[3]));
247+
});
237248
}
238249
});
239250

@@ -242,10 +253,11 @@ public void executeSinglePlayer(CommandSender sender, String[] args) {
242253

243254
@Override
244255
public void execute(CommandSender sender, String[] args) {
245-
AdvancedCoreUser user = plugin.getUserManager().getUser(args[1]);
246-
plugin.getRewardHandler().giveReward(user, args[2],
247-
new RewardOptions().setOnline(user.isOnline()).addPlaceholder(args[3], args[4]));
248-
sender.sendMessage("&cGave " + args[1] + " the reward file " + args[2]);
256+
withResolvedUser(sender, args[1], user -> {
257+
plugin.getRewardHandler().giveReward(user, args[2],
258+
new RewardOptions().setOnline(user.isOnline()).addPlaceholder(args[3], args[4]));
259+
runCommandCallback(sender, () -> sender.sendMessage("&cGave " + args[1] + " the reward file " + args[2]));
260+
});
249261
}
250262
});
251263

@@ -298,12 +310,12 @@ public void execute(CommandSender sender, String[] args) {
298310
public void execute(CommandSender sender, String[] args) {
299311
sendMessage(sender, "&cStarting to run offline rewards for " + args[1]);
300312

301-
AdvancedCoreUser user = plugin.getUserManager().getUser(args[1]);
302-
user.userDataFetechMode(UserDataFetchMode.NO_CACHE);
303-
304-
user.forceRunOfflineRewards();
305-
306-
sendMessage(sender, "&cFinished running offline rewards for " + args[1]);
313+
withResolvedUser(sender, args[1], user -> {
314+
user.userDataFetechMode(UserDataFetchMode.NO_CACHE);
315+
user.forceRunOfflineRewards();
316+
runCommandCallback(sender,
317+
() -> sendMessage(sender, "&cFinished running offline rewards for " + args[1]));
318+
});
307319
}
308320
});
309321

@@ -407,11 +419,8 @@ public void execute(CommandSender sender, String[] args) {
407419
public void execute(CommandSender sender, String[] args) {
408420
sendMessage(sender, "&cRemoving " + args[1]);
409421

410-
AdvancedCoreUser user = plugin.getUserManager().getUser(args[1]);
411-
removeUserData(sender, args[1], user, () -> {
412-
String uuidStr = UuidLookup.getInstance().getUUID(args[1]);
413-
UuidLookup.getInstance().invalidate(isBlank(uuidStr) ? args[1] : uuidStr);
414-
});
422+
withResolvedUser(sender, args[1], user -> removeUserData(sender, args[1], user,
423+
() -> UuidLookup.getInstance().invalidate(user.getUUID())));
415424
}
416425
});
417426

@@ -527,13 +536,14 @@ public void executeAll(CommandSender sender, String[] args) {
527536

528537
@Override
529538
public void executeSinglePlayer(CommandSender sender, String[] args) {
530-
AdvancedCoreUser user = plugin.getUserManager().getUser(args[1]);
531-
String data = args[4];
532-
if (data.equalsIgnoreCase("\"\"")) {
533-
data = "";
534-
}
535-
user.getData().setString(args[3], data);
536-
sender.sendMessage(MessageAPI.colorize("&cSet " + args[3] + " for " + args[1] + " to " + args[4]));
539+
String data = args[4];
540+
if (data.equalsIgnoreCase("\"\"")) data = "";
541+
final String value = data;
542+
withResolvedUser(sender, args[1], user -> {
543+
user.getData().setString(args[3], value);
544+
runCommandCallback(sender, () -> sender.sendMessage(
545+
MessageAPI.colorize("&cSet " + args[3] + " for " + args[1] + " to " + args[4])));
546+
});
537547
}
538548
}.withLegacyAllPermissionAliases(permPrefix + ".SetAllData"));
539549

@@ -542,27 +552,28 @@ public void executeSinglePlayer(CommandSender sender, String[] args) {
542552

543553
@Override
544554
public void execute(CommandSender sender, String[] args) {
545-
AdvancedCoreUser user = plugin.getUserManager().getUser(args[1]);
546-
if (plugin.getUserManager().getDataManager().deferSharedStorageResult(user.getData()::getValues,
547-
values -> values.forEach((key, value) ->
548-
sendMessage(sender, "&c&l" + key + " &c" + value.toString())),
549-
failure -> sendMessage(sender, "&cUnable to read user data; check the server log."),
550-
callbackOwner(sender))) return;
551-
for (Entry<String, DataValue> entry : user.getData().getValues().entrySet()) {
552-
sendMessage(sender, "&c&l" + entry.getKey() + " &c" + entry.getValue().toString());
553-
}
555+
withResolvedUser(sender, args[1], user -> {
556+
if (plugin.getUserManager().getDataManager().deferSharedStorageResult(user.getData()::getValues,
557+
values -> values.forEach((key, value) ->
558+
sendMessage(sender, "&c&l" + key + " &c" + value.toString())),
559+
failure -> sendMessage(sender, "&cUnable to read user data; check the server log."),
560+
callbackOwner(sender))) return;
561+
for (Entry<String, DataValue> entry : user.getData().getValues().entrySet()) {
562+
sendMessage(sender, "&c&l" + entry.getKey() + " &c" + entry.getValue().toString());
563+
}
564+
});
554565
}
555566
});
556567

557568
cmds.add(new CommandHandler(plugin, new String[] { "User", "(Player)", "ViewCache" }, permPrefix + ".ViewCache",
558569
"View playerdata") {
559570

560571
@Override
561-
public void execute(CommandSender sender, String[] args) {
562-
AdvancedCoreUser user = plugin.getUserManager().getUser(args[1]);
563-
for (String str : user.getCache().displayCacheStringList()) {
564-
sender.sendMessage(str);
565-
}
572+
public void execute(CommandSender sender, String[] args) {
573+
withResolvedUser(sender, args[1], user -> {
574+
java.util.List<String> entries = user.getCache().displayCacheStringList();
575+
runCommandCallback(sender, () -> entries.forEach(sender::sendMessage));
576+
});
566577

567578
}
568579
});
@@ -572,9 +583,10 @@ public void execute(CommandSender sender, String[] args) {
572583

573584
@Override
574585
public void execute(CommandSender sender, String[] args) {
575-
AdvancedCoreUser user = plugin.getUserManager().getUser(args[1]);
576-
user.cache();
577-
sendMessage(sender, "&aForced cached " + args[1]);
586+
withResolvedUser(sender, args[1], user -> {
587+
user.cache();
588+
runCommandCallback(sender, () -> sendMessage(sender, "&aForced cached " + args[1]));
589+
});
578590
}
579591
});
580592

@@ -583,8 +595,11 @@ public void execute(CommandSender sender, String[] args) {
583595

584596
@Override
585597
public void execute(CommandSender sender, String[] args) {
586-
AdvancedCoreUser user = plugin.getUserManager().getUser(args[1]);
587-
sendMessage(sender, "User " + args[1] + " permission " + args[3] + ":" + user.hasPermission(args[3]));
598+
withResolvedUser(sender, args[1], user -> {
599+
boolean hasPermission = user.hasPermission(args[3]);
600+
runCommandCallback(sender, () -> sendMessage(sender,
601+
"User " + args[1] + " permission " + args[3] + ":" + hasPermission));
602+
});
588603
}
589604
});
590605

@@ -594,10 +609,10 @@ public void execute(CommandSender sender, String[] args) {
594609

595610
@Override
596611
public void execute(CommandSender sender, String[] args) {
597-
AdvancedCoreUser user = plugin.getUserManager().getUser(args[4]);
598-
user.setChoicePreference(args[2], args[3]);
599-
600-
user.sendMessage("&cPreference set to " + args[3] + " for " + args[4]);
612+
withResolvedUser(sender, args[4], user -> {
613+
user.setChoicePreference(args[2], args[3]);
614+
user.sendMessage("&cPreference set to " + args[3] + " for " + args[4]);
615+
});
601616
}
602617
});
603618

0 commit comments

Comments
 (0)