Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4417d55
Add shared user cache ownership port
BenCodez Sep 13, 2026
216acbf
Add shared user data lifecycle runtime
BenCodez Sep 13, 2026
b06565a
Adapt existing Bukkit user cache ownership
BenCodez Sep 13, 2026
fda552c
Test shared user lifecycle and queue semantics
BenCodez Sep 13, 2026
ee8c5e0
Coordinate shared cache writes, provider replacement and asynchronous…
BenCodez Sep 13, 2026
6cdb670
Fix nested Mockito stubbing in shutdown barrier regression
BenCodez Sep 13, 2026
5442c37
Fix late cache ownership, cache-only reads and atomic attachment
BenCodez Sep 13, 2026
6360c4f
Allow cache-only Bukkit operations and restack shared runtime
BenCodez Sep 13, 2026
0bf3a2b
Restack shared user runtime onto corrected SQL head
BenCodez Sep 13, 2026
f0fba33
Route legacy user data through shared backend safely
BenCodez Sep 13, 2026
e732e4b
Restack shared user runtime onto current SQL backend
BenCodez Sep 13, 2026
8eb0984
Preserve newer cache values across completed-write population races
BenCodez Sep 13, 2026
f91009a
Preserve legacy SQL access before optional cache-manager initialization
BenCodez Sep 13, 2026
4c6b8ce
Add per-user lifecycle gate to cache owner
BenCodez Sep 13, 2026
1f46458
Publish shared SQL route atomically
BenCodez Sep 13, 2026
b895abb
Make shared cache refresh race-safe
BenCodez Sep 13, 2026
36370dd
Bind caches through per-user lifecycle barrier
BenCodez Sep 13, 2026
e7e8adc
Serialize per-user removal against writes
BenCodez Sep 13, 2026
8cbfc50
Fix shared user lifecycle follow-up regressions
BenCodez Sep 13, 2026
41adcc4
Fix shared user review follow-ups
BenCodez Sep 13, 2026
de72339
Close shared user lifecycle race windows
BenCodez Sep 13, 2026
9c160c5
Fix backend replacement routing races
BenCodez Sep 13, 2026
b4225f5
Close shared user storage transition gaps
BenCodez Sep 13, 2026
6882a8a
Close shared cache lifecycle races
BenCodez Sep 13, 2026
fdb9bff
Close cache transition lifecycle gaps
BenCodez Sep 13, 2026
dd7e442
Harden legacy cache refresh and batching
BenCodez Sep 13, 2026
f16db51
Integrate shared user cache runtime
BenCodez Sep 14, 2026
e560931
Harden shared user storage lifecycle
BenCodez Sep 15, 2026
1fcd6a7
Merge master and harden shared user lifecycle
BenCodez Sep 15, 2026
c888f24
Harden shared user replay lifecycle
BenCodez Sep 15, 2026
4d75c4d
Harden cache reads and shutdown ownership
BenCodez Sep 15, 2026
fc2edf8
Preserve shared user state across async boundaries
BenCodez Sep 15, 2026
2793198
Snapshot deferred user data inputs
BenCodez Sep 15, 2026
1b3d0fa
Fence synchronous cache publication
BenCodez Sep 15, 2026
2772087
Bound deferred storage shutdown
BenCodez Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
branches: [ "master", "codex/shared-sql-backend-init" ]

permissions:
contents: write # required for dependency submission
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
import java.util.LinkedList;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.UUID;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -43,10 +45,12 @@
import com.bencodez.advancedcore.api.time.TimeChecker;
import com.bencodez.advancedcore.api.time.TimeType;
import com.bencodez.advancedcore.api.user.AdvancedCoreUser;
import com.bencodez.advancedcore.api.user.UserDataFetchMode;
import com.bencodez.advancedcore.api.user.UserManager;
import com.bencodez.advancedcore.api.user.UserStartup;
import com.bencodez.advancedcore.api.user.UserStorage;
import com.bencodez.advancedcore.api.user.UserDataFetchMode;
import com.bencodez.advancedcore.api.user.UserManager;
import com.bencodez.advancedcore.api.user.UserStartup;
import com.bencodez.advancedcore.api.user.UserStorage;
import com.bencodez.advancedcore.api.user.usercache.UserDataManager;
import com.bencodez.advancedcore.bukkit.user.runtime.BukkitUserRuntimeBootstrap;
import com.bencodez.advancedcore.api.user.userstorage.mysql.MySQL;
import com.bencodez.advancedcore.api.user.userstorage.sql.UserTable;
import com.bencodez.advancedcore.command.CommandLoader;
Expand Down Expand Up @@ -508,10 +512,35 @@ public void run() {
* @param from the source storage type
* @param to the target storage type
*/
public void convertDataStorage(UserStorage from, UserStorage to) {
debug("Starting convert process");
if (to == null) {
throw new RuntimeException("Invalid Storage Method");
public void convertDataStorage(UserStorage from, UserStorage to) {
if (Bukkit.getServer() != null && Bukkit.isPrimaryThread()) {
throw new IllegalStateException("User storage conversion must run asynchronously; use convertDataStorageAsync");
}
getUserManager().getDataManager().runStorageMaintenance(() -> convertDataStorageNow(from, to));
}

/**
* Start an explicit SQL-to-SQL conversion without blocking the server thread.
* The result completes after the shared cache generation was flushed and the
* converter has finished; callers must not report success before then.
*/
public CompletionStage<Void> convertDataStorageAsync(UserStorage from, UserStorage to) {
CompletableFuture<Void> result = new CompletableFuture<>();
try {
getBukkitScheduler().runTaskAsynchronously(this, () -> {
try {
convertDataStorage(from, to);
result.complete(null);
} catch (Throwable failure) { result.completeExceptionally(failure); }
});
} catch (RuntimeException | Error failure) { result.completeExceptionally(failure); }
return result;
}

private void convertDataStorageNow(UserStorage from, UserStorage to) {
debug("Starting convert process");
if (to == null) {
throw new RuntimeException("Invalid Storage Method");
}
loadUserAPI(from);
loadUserAPI(to);
Expand Down Expand Up @@ -630,12 +659,15 @@ public UserStorage getStorageType() {
*
* @return the user manager
*/
public UserManager getUserManager() {
public UserManager getUserManager() {
if (userManager == null) {
userManager = new UserManager(this);
}
return userManager;
}
return userManager;
}

/** Existing manager only; shutdown must not allocate a new user subsystem. */
public UserManager getLoadedUserManager() { return userManager; }

private YamlConfiguration getVersionFile() {
try {
Expand Down Expand Up @@ -690,12 +722,19 @@ public void loadAdvancedCoreEvents() {
Bukkit.getPluginManager().registerEvents(new BInventoryListener(this), this);
}

private void loadConfig(boolean userStorage) {
getOptions().load(this);
if (loadUserData && userStorage) {
loadUserAPI(getOptions().getStorageType());
}
}
private void loadConfig(boolean userStorage) {
getOptions().load(this);
if (loadUserData && userStorage) {
loadUserAPI(getOptions().getStorageType());
bindSharedUserRuntime();
}
}

/** Bind only after the native Bukkit storage owner has initialized successfully. */
private void bindSharedUserRuntime() {
UserDataManager manager = getUserManager().getDataManager();
BukkitUserRuntimeBootstrap.bindAfterStorageInitialization(this, manager);
}

private void loadHandle() {

Expand Down Expand Up @@ -1023,11 +1062,12 @@ public void updateReplacements() {
*
* @param storageType the storage type to load
*/
public void loadUserAPI(UserStorage storageType) {
if (storageType == null) {
throw new IllegalArgumentException("User storage must be SQLITE or MYSQL");
}
if (storageType.equals(UserStorage.SQLITE)) {
public void loadUserAPI(UserStorage storageType) {
if (storageType == null) {
throw new IllegalArgumentException("User storage must be SQLITE or MYSQL");
}
requireUserStorageMaintenanceWindow();
if (storageType.equals(UserStorage.SQLITE)) {
ArrayList<Column> columns = new ArrayList<>();
Column key = new Column("uuid", DataType.STRING);
columns.add(key);
Expand All @@ -1046,7 +1086,22 @@ public void loadUserAPI(UserStorage storageType) {
}

}
}
}

/**
* The shared runtime owns cache flushing and the lifecycle admission for the
* native SQL provider. Replacing that provider in-place would let an in-flight
* flush target a connection that reload has already replaced or closed. There
* is no safe synchronous Bukkit hot-reload boundary for this today, so require
* a full plugin restart before mutating either native storage owner.
*/
private void requireUserStorageMaintenanceWindow() {
UserManager loadedUsers = getLoadedUserManager();
if (loadedUsers != null && loadedUsers.getDataManager().hasSharedRuntimeLifecycle()
&& !loadedUsers.getDataManager().isStorageMaintenanceActive()) {
throw new IllegalStateException("User storage reload requires a full plugin restart while shared user storage is active or retiring");
}
}

private void loadUUIDs() {

Expand Down Expand Up @@ -1206,8 +1261,9 @@ public void reloadAdvancedCore() {
*
* @param userStorage whether to reload user storage
*/
public void reloadAdvancedCore(boolean userStorage) {
getServerDataFile().reloadData();
public void reloadAdvancedCore(boolean userStorage) {
if (userStorage) requireUserStorageMaintenanceWindow();
Comment on lines +1264 to +1265

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the public user-storage reload operation

After ordinary startup, the shared runtime lifecycle is always active, so every downstream invocation of the documented public reloadAdvancedCore(true) method now throws at this guard before reloading anything. This is an unconditional compatibility break for plugins that previously requested a user-storage reload, without an authorized breaking migration or a replacement completion-based API. Preserve the operation through a safe maintenance transition or provide a compatible asynchronous replacement before rejecting the old entry point.

AGENTS.md reference: AGENTS.md:L27-L27

Useful? React with 👍 / 👎.

getServerDataFile().reloadData();
rewardHandler.loadRewards();
loadConfig(userStorage);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,10 @@ public String getPlayerName(AdvancedCoreUser user, String uuid, boolean useCache
cacheMapping(uuid, liveName);

// Update stored PlayerName if it changed / missing
if (user != null && user.getUserData().hasData()) {
if (storedName.isEmpty() || storedName.equalsIgnoreCase("Error getting name")
|| !liveName.equals(storedName)) {
user.getData().setString("PlayerName", liveName);
}
if (user != null && (storedName.isEmpty() || storedName.equalsIgnoreCase("Error getting name")
|| !liveName.equals(storedName))) {
user.setPlayerName(liveName);
user.updateName(false);
}
return liveName;
}
Expand Down
Loading
Loading