5151import com .bencodez .advancedcore .api .user .UserStorage ;
5252import com .bencodez .advancedcore .api .user .usercache .UserDataManager ;
5353import com .bencodez .advancedcore .bukkit .user .runtime .BukkitUserRuntimeBootstrap ;
54+ import com .bencodez .advancedcore .bukkit .user .storage .BukkitSqlUserBackend ;
5455import com .bencodez .advancedcore .api .user .userstorage .mysql .MySQL ;
5556import com .bencodez .advancedcore .api .user .userstorage .sql .UserTable ;
5657import com .bencodez .advancedcore .command .CommandLoader ;
@@ -189,7 +190,24 @@ public static void setInstance(AdvancedCorePlugin plugin) {
189190 @ Getter
190191 private CMIHandler cmiHandle ;
191192
192- private Database database ;
193+ private Database database ;
194+ /**
195+ * A coherent native-owner snapshot for callers which need both the selected
196+ * storage kind and its provider. Shared-runtime replacement publishes this
197+ * as one volatile value; callers must not combine a separately observed
198+ * storage type with the mutable provider fields.
199+ */
200+ public record UserStorageOwner (UserStorage storageType , MySQL mysql , UserTable table ) {
201+ public UserStorageOwner {
202+ if (storageType == null ) throw new IllegalArgumentException ("storageType" );
203+ if (storageType == UserStorage .MYSQL && mysql == null ) throw new IllegalArgumentException ("mysql" );
204+ if (storageType == UserStorage .SQLITE && table == null ) throw new IllegalArgumentException ("table" );
205+ }
206+ }
207+ private volatile UserStorageOwner nativeUserStorageOwner ;
208+ /** Coalesces public storage reload requests while a replacement is being prepared. */
209+ private Object userStorageReloadLock = new Object ();
210+ private CompletionStage <Void > userStorageReload ;
193211
194212 /**
195213 * Handler for full inventory management.
@@ -631,7 +649,7 @@ public void extraDebug(String debug) {
631649 *
632650 * @return the user table, or null if not using SQLite
633651 */
634- public UserTable getSQLiteUserTable () {
652+ public UserTable getSQLiteUserTable () {
635653 if (database == null && loadUserData ) {
636654 loadUserAPI (getStorageType ());
637655 }
@@ -642,17 +660,38 @@ public UserTable getSQLiteUserTable() {
642660 }
643661 }
644662 }
645- return null ;
646- }
663+ return null ;
664+ }
665+
666+ /**
667+ * Returns the current native provider as one immutable observation. This is
668+ * intentionally separate from the legacy individual getters: user-facing
669+ * bulk APIs use this snapshot while a shared route is being replaced.
670+ */
671+ public UserStorageOwner getNativeUserStorageOwner () {
672+ UserStorageOwner owner = nativeUserStorageOwner ;
673+ if (owner != null ) return owner ;
674+ UserStorage configured = getOptions ().getStorageType ();
675+ if (configured == UserStorage .MYSQL && mysql != null ) return new UserStorageOwner (configured , mysql , null );
676+ if (configured == UserStorage .SQLITE && database != null ) {
677+ for (Table table : database .getTables ()) {
678+ if (table instanceof UserTable userTable ) return new UserStorageOwner (configured , null , userTable );
679+ }
680+ }
681+ return null ;
682+ }
647683
648684 /**
649685 * Gets the current storage type configuration.
650686 *
651687 * @return the storage type
652688 */
653- public UserStorage getStorageType () {
654- return getOptions ().getStorageType ();
655- }
689+ public UserStorage getStorageType () {
690+ UserStorage configured = getOptions ().getStorageType ();
691+ UserManager loadedUsers = getLoadedUserManager ();
692+ return loadedUsers == null ? configured
693+ : loadedUsers .getDataManager ().effectiveStorageType (configured );
694+ }
656695
657696 /**
658697 * Gets the user manager instance.
@@ -1072,8 +1111,9 @@ public void loadUserAPI(UserStorage storageType) {
10721111 Column key = new Column ("uuid" , DataType .STRING );
10731112 columns .add (key );
10741113 UserTable table = new UserTable (this , "Users" , columns , key );
1075- database = new Database (this , "Users" , table );
1076- table .addCustomColumns ();
1114+ database = new Database (this , "Users" , table );
1115+ table .addCustomColumns ();
1116+ nativeUserStorageOwner = new UserStorageOwner (UserStorage .SQLITE , null , table );
10771117 } else if (storageType .equals (UserStorage .MYSQL )) {
10781118 if (getOptions ().getYmlConfig ().getData ().contains ("Database" )) {
10791119 setMysql (new MySQL (javaPlugin , javaPlugin .getName () + "_Users" ,
@@ -1256,36 +1296,212 @@ public void reloadAdvancedCore() {
12561296 reloadAdvancedCore (false );
12571297 }
12581298
1259- /**
1260- * Reloads AdvancedCore configuration.
1261- *
1262- * @param userStorage whether to reload user storage
1263- */
1299+ /**
1300+ * Starts an AdvancedCore configuration reload.
1301+ *
1302+ * <p>When {@code userStorage} is true and a shared runtime is active, this
1303+ * method only starts the non-blocking reload; it returns before storage has
1304+ * been flushed, replaced, or confirmed. It deliberately has no success
1305+ * signal. Call {@link #reloadAdvancedCoreAsync(boolean)} when a caller must
1306+ * continue, notify an administrator, or report status after completion.
1307+ *
1308+ * @param userStorage whether to reload user storage
1309+ */
12641310 public void reloadAdvancedCore (boolean userStorage ) {
1311+ if (userStorage && hasActiveSharedUserRuntime ()) {
1312+ reloadAdvancedCoreAsync (true ).whenComplete ((ignored , failure ) -> {
1313+ if (failure != null ) {
1314+ getLogger ().warning ("User storage reload did not complete: " + failure .getMessage ());
1315+ debug (failure );
1316+ }
1317+ });
1318+ return ;
1319+ }
1320+ reloadAdvancedCoreNow (userStorage );
1321+ }
1322+
1323+ /**
1324+ * Reload AdvancedCore without making a Bukkit thread wait for a shared user
1325+ * storage flush or a database connection. Callers that need to report a
1326+ * confirmed storage reload should await this stage rather than assuming that
1327+ * the legacy void overload has completed synchronously.
1328+ *
1329+ * @param userStorage whether to reload user storage
1330+ * @return completion of the reload, including the shared backend replacement
1331+ */
1332+ public CompletionStage <Void > reloadAdvancedCoreAsync (boolean userStorage ) {
1333+ if (!userStorage || !hasActiveSharedUserRuntime ()) {
1334+ CompletableFuture <Void > completion = new CompletableFuture <>();
1335+ try {
1336+ reloadAdvancedCoreNow (userStorage );
1337+ completion .complete (null );
1338+ } catch (Throwable failure ) { completion .completeExceptionally (failure ); }
1339+ return completion ;
1340+ }
1341+ synchronized (userStorageReloadLock ()) {
1342+ if (userStorageReload != null && !userStorageReload .toCompletableFuture ().isDone ()) return userStorageReload ;
1343+ CompletableFuture <Void > completion = new CompletableFuture <>();
1344+ userStorageReload = completion ;
1345+ try {
1346+ getBukkitScheduler ().runTask (this , () -> beginSharedUserStorageReload (completion ));
1347+ } catch (RuntimeException | Error failure ) {
1348+ completion .completeExceptionally (failure );
1349+ userStorageReload = null ;
1350+ }
1351+ return completion ;
1352+ }
1353+ }
1354+
1355+ private boolean hasActiveSharedUserRuntime () {
1356+ UserManager users = getLoadedUserManager ();
1357+ return users != null && users .getDataManager ().hasSharedRuntime ();
1358+ }
1359+
1360+ private void beginSharedUserStorageReload (CompletableFuture <Void > completion ) {
1361+ try {
1362+ getServerDataFile ().reloadData ();
1363+ rewardHandler .loadRewards ();
1364+ getOptions ().load (this );
1365+ getBukkitScheduler ().runTaskAsynchronously (this , () -> replaceSharedUserStorage (completion ));
1366+ } catch (Throwable failure ) {
1367+ completion .completeExceptionally (failure );
1368+ clearSharedUserStorageReload (completion );
1369+ }
1370+ }
1371+
1372+ private void replaceSharedUserStorage (CompletableFuture <Void > completion ) {
1373+ UserStorageReplacement replacement = null ;
1374+ try {
1375+ replacement = prepareUserStorageReplacement (getOptions ().getStorageType ());
1376+ UserStorageReplacement prepared = replacement ;
1377+ getUserManager ().getDataManager ().replaceSharedSqlBackendAsync (prepared .backend (),
1378+ () -> installUserStorageReplacement (prepared )).whenComplete ((ignored , failure ) -> {
1379+ if (failure != null ) {
1380+ // SharedUserDataRuntime reports an exception here only before it
1381+ // publishes the replacement route. A post-publication old-owner close
1382+ // is retained there for retry and deliberately completes successfully.
1383+ prepared .closeUnpublished ();
1384+ completion .completeExceptionally (failure );
1385+ clearSharedUserStorageReload (completion );
1386+ return ;
1387+ }
1388+ try {
1389+ getBukkitScheduler ().runTask (this , () -> completeSharedUserStorageReload (completion ));
1390+ } catch (Throwable completionFailure ) {
1391+ completion .completeExceptionally (completionFailure );
1392+ clearSharedUserStorageReload (completion );
1393+ }
1394+ });
1395+ } catch (Throwable failure ) {
1396+ if (replacement != null ) replacement .closeUnpublished ();
1397+ completion .completeExceptionally (failure );
1398+ clearSharedUserStorageReload (completion );
1399+ }
1400+ }
1401+
1402+ private void completeSharedUserStorageReload (CompletableFuture <Void > completion ) {
1403+ try {
1404+ finishReloadAdvancedCore ();
1405+ completion .complete (null );
1406+ } catch (Throwable failure ) { completion .completeExceptionally (failure ); }
1407+ finally { clearSharedUserStorageReload (completion ); }
1408+ }
1409+
1410+ private void clearSharedUserStorageReload (CompletableFuture <Void > completion ) {
1411+ synchronized (userStorageReloadLock ()) {
1412+ if (userStorageReload == completion ) userStorageReload = null ;
1413+ }
1414+ }
1415+
1416+ private Object userStorageReloadLock () {
1417+ Object lock = userStorageReloadLock ;
1418+ if (lock != null ) return lock ;
1419+ synchronized (this ) {
1420+ if (userStorageReloadLock == null ) userStorageReloadLock = new Object ();
1421+ return userStorageReloadLock ;
1422+ }
1423+ }
1424+
1425+ private UserStorageReplacement prepareUserStorageReplacement (UserStorage storageType ) {
1426+ if (storageType == null ) throw new IllegalArgumentException ("User storage must be SQLITE or MYSQL" );
1427+ if (storageType == UserStorage .SQLITE ) {
1428+ ArrayList <Column > columns = new ArrayList <>();
1429+ Column key = new Column ("uuid" , DataType .STRING );
1430+ columns .add (key );
1431+ UserTable table = new UserTable (this , "Users" , columns , key );
1432+ Database replacementDatabase = new Database (this , "Users" , table );
1433+ table .addCustomColumns ();
1434+ return new UserStorageReplacement (storageType , replacementDatabase , null ,
1435+ new BukkitSqlUserBackend (this , storageType , null , table ));
1436+ }
1437+ ConfigurationSection section = getOptions ().getYmlConfig ().getData ()
1438+ .getConfigurationSection (getOptions ().getYmlConfig ().getData ().contains ("Database" ) ? "Database" : "MySQL" );
1439+ if (section == null ) throw new IllegalStateException ("MySQL user storage configuration is missing" );
1440+ MySQL replacementMysql = new MySQL (javaPlugin , javaPlugin .getName () + "_Users" , section );
1441+ return new UserStorageReplacement (storageType , null , replacementMysql ,
1442+ new BukkitSqlUserBackend (this , storageType , replacementMysql , null ));
1443+ }
1444+
1445+ private void installUserStorageReplacement (UserStorageReplacement replacement ) {
1446+ MySQL previousMysql = mysql ;
1447+ Database previousDatabase = database ;
1448+ mysql = replacement .mysql ();
1449+ database = replacement .database ();
1450+ // Publish only after both legacy fields have been assigned. The shared route
1451+ // picks this exact snapshot up in the same lifecycle write admission.
1452+ nativeUserStorageOwner = replacement .owner ();
1453+ if (previousMysql != null && previousMysql != mysql ) {
1454+ try { previousMysql .close (); }
1455+ catch (RuntimeException | Error closeFailure ) { debug (closeFailure ); }
1456+ }
1457+ if (previousDatabase != null && previousDatabase != database ) {
1458+ try { previousDatabase .getDB ().closeConnection (); }
1459+ catch (RuntimeException | Error closeFailure ) { debug (closeFailure ); }
1460+ }
1461+ }
1462+
1463+ private void reloadAdvancedCoreNow (boolean userStorage ) {
12651464 if (userStorage ) requireUserStorageMaintenanceWindow ();
12661465 getServerDataFile ().reloadData ();
1267- rewardHandler .loadRewards ();
1268- loadConfig (userStorage );
1269-
1270- if (userStorage ) {
1271- getUserManager ().getDataManager ().clearCache ();
1272- if (getStorageType ().equals (UserStorage .MYSQL ) && getMysql () != null ) {
1273- getMysql ().clearCacheBasic ();
1274- }
1275- }
1276- timeChecker .update ();
1277- TabCompleteHandler .getInstance ().reload ();
1278- TabCompleteHandler .getInstance ().loadTabCompleteOptions ();
1279- getRewardHandler ().checkSubRewards ();
1280-
1281- if (skullCacheHandler != null ) {
1282- if (!getOptions ().getSkullProfileAPIURL ().isEmpty ()) {
1283- debug ("Setting API profile URL to " + getOptions ().getSkullProfileAPIURL ());
1284- skullCacheHandler .changeApiProfileURL (getOptions ().getSkullProfileAPIURL ());
1285- }
1286- getSkullCacheHandler ().setBedrockPrefix (getOptions ().getBedrockPlayerPrefix ());
1287- }
1288- }
1466+ rewardHandler .loadRewards ();
1467+ loadConfig (userStorage );
1468+ if (userStorage ) {
1469+ getUserManager ().getDataManager ().clearCache ();
1470+ if (getStorageType ().equals (UserStorage .MYSQL ) && getMysql () != null ) getMysql ().clearCacheBasic ();
1471+ }
1472+ finishReloadAdvancedCore ();
1473+ }
1474+
1475+ private void finishReloadAdvancedCore () {
1476+ timeChecker .update ();
1477+ TabCompleteHandler .getInstance ().reload ();
1478+ TabCompleteHandler .getInstance ().loadTabCompleteOptions ();
1479+ getRewardHandler ().checkSubRewards ();
1480+ if (skullCacheHandler != null ) {
1481+ if (!getOptions ().getSkullProfileAPIURL ().isEmpty ()) {
1482+ debug ("Setting API profile URL to " + getOptions ().getSkullProfileAPIURL ());
1483+ skullCacheHandler .changeApiProfileURL (getOptions ().getSkullProfileAPIURL ());
1484+ }
1485+ getSkullCacheHandler ().setBedrockPrefix (getOptions ().getBedrockPlayerPrefix ());
1486+ }
1487+ }
1488+
1489+ private record UserStorageReplacement (UserStorage storageType , Database database , MySQL mysql ,
1490+ BukkitSqlUserBackend backend ) {
1491+ private UserStorageOwner owner () {
1492+ return new UserStorageOwner (storageType , mysql , database == null ? null : findUserTable (database ));
1493+ }
1494+
1495+ private static UserTable findUserTable (Database database ) {
1496+ for (Table table : database .getTables ()) if (table instanceof UserTable userTable ) return userTable ;
1497+ throw new IllegalStateException ("Replacement SQLite user table is unavailable" );
1498+ }
1499+ private void closeUnpublished () {
1500+ backend .close ();
1501+ if (mysql != null ) mysql .close ();
1502+ if (database != null ) database .getDB ().closeConnection ();
1503+ }
1504+ }
12891505
12901506 /**
12911507 * @param configData the configData to set
@@ -1325,13 +1541,14 @@ public void setConfigData(YMLConfig ymlConfig) {
13251541 *
13261542 * @param mysql the mysql connection to set
13271543 */
1328- public void setMysql (MySQL mysql ) {
1544+ public void setMysql (MySQL mysql ) {
13291545 if (this .mysql != null ) {
13301546 this .mysql .close ();
13311547 this .mysql = null ;
1332- }
1333- this .mysql = mysql ;
1334- }
1548+ }
1549+ this .mysql = mysql ;
1550+ if (mysql != null ) nativeUserStorageOwner = new UserStorageOwner (UserStorage .MYSQL , mysql , null );
1551+ }
13351552
13361553 /**
13371554 * Runs user startup tasks.
0 commit comments