@@ -339,6 +339,9 @@ public String formatVoteCommandMessage(String message) {
339339
340340 private volatile VotingPluginBackgroundTask backgroundTask ;
341341 private volatile AtomicBoolean basicBungeeUpdateRunning ;
342+ private final AtomicBoolean unloadInProgress = new AtomicBoolean ();
343+ private Set <String > completedUnloadSteps ;
344+ private java .util .concurrent .CompletionStage <Void > signShutdownPublication ;
342345
343346 private synchronized AtomicBoolean basicBungeeUpdateAdmission () {
344347 if (basicBungeeUpdateRunning == null ) basicBungeeUpdateRunning = new AtomicBoolean ();
@@ -794,6 +797,7 @@ private void metrics() {
794797
795798 @ Override
796799 public void onPostLoad () {
800+ serverData .refreshVotePartyDisplay ();
797801 ensureCommunicationSecret ();
798802 // auto conversion for Shop.yml
799803 if (plugin .getShopFile ().isJustCreated ()) {
@@ -2011,9 +2015,95 @@ private void migrateVoteBroadcast(Config configFile) {
20112015 configFile .saveData ();
20122016 }
20132017
2018+ @ Getter
2019+ private volatile com .bencodez .votingplugin .util .ServerDataPublicationFence serverDataPublicationFence ;
2020+
2021+ private volatile com .bencodez .votingplugin .data .GenerationFencedServerDataFile fencedServerDataFile ;
2022+
2023+ private java .util .concurrent .CompletableFuture <Void > serverDataFinalPublication ;
2024+ private boolean serverDataFinalPublicationReserved ;
2025+ private boolean serverDataFinalStageAttached ;
2026+ private volatile boolean serverDataEnableCleanupStarted ;
2027+ private volatile boolean serverDataEnableAdmissionRejected ;
2028+
2029+ @ Override
2030+ public com .bencodez .advancedcore .data .ServerData getServerDataFile () {
2031+ var file = super .getServerDataFile ();
2032+ var current = fencedServerDataFile ;
2033+ if (current != null && current .wraps (file )) {
2034+ // The SDK replaces its file after pre-load. A failed new generation has
2035+ // no file to finalize; never expose the predecessor as its current owner.
2036+ return current .belongsTo (serverDataPublicationFence ) ? current : null ;
2037+ }
2038+ synchronized (this ) {
2039+ file = super .getServerDataFile ();
2040+ var owner = serverDataPublicationFence ;
2041+ if (file == null || owner == null ) return file ;
2042+ current = fencedServerDataFile ;
2043+ if (current != null && current .wraps (file ) && !current .belongsTo (owner )) return null ;
2044+ if (current == null || !current .wraps (file )) {
2045+ current = new com .bencodez .votingplugin .data .GenerationFencedServerDataFile (this , file , owner );
2046+ fencedServerDataFile = current ;
2047+ }
2048+ return current ;
2049+ }
2050+ }
2051+
2052+ /** Capture the existing AdvancedCore executors which can still publish server-data state. */
2053+ public ScheduledExecutorService [] getServerDataWriterExecutors () {
2054+ var checker = getTimeChecker ();
2055+ return new ScheduledExecutorService [] { getTimer (), getLoginTimer (), getInventoryTimer (),
2056+ checker == null ? null : checker .getTimer () };
2057+ }
2058+
2059+ @ Override
2060+ public void onEnable () {
2061+ synchronized (this ) {
2062+ // Bind a retained SDK file before changing owner. AdvancedCore replaces its
2063+ // private file only later in loadHook(), after onPreLoad().
2064+ if (serverDataPublicationFence != null ) getServerDataFile ();
2065+ serverDataPublicationFence = com .bencodez .votingplugin .util .ServerDataPublicationFence .beginGeneration (
2066+ getDataFolder ().toPath ().resolve ("ServerData.yml" ));
2067+ try { serverDataPublicationFence .retainForProcessExit (); }
2068+ catch (RuntimeException | Error registrationFailure ) {
2069+ serverDataEnableAdmissionRejected = true ;
2070+ try { serverDataPublicationFence .retire (); }
2071+ catch (RuntimeException | Error cleanupFailure ) {
2072+ if (cleanupFailure != registrationFailure ) registrationFailure .addSuppressed (cleanupFailure );
2073+ }
2074+ throw registrationFailure ;
2075+ }
2076+ serverDataFinalPublication = new java .util .concurrent .CompletableFuture <>();
2077+ serverDataFinalPublicationReserved = false ;
2078+ serverDataFinalStageAttached = false ;
2079+ serverDataEnableCleanupStarted = false ;
2080+ serverDataEnableAdmissionRejected = false ;
2081+ }
2082+ // Failed startup can already own SDK writers; disable must retire them before replacement.
2083+ try { super .onEnable (); }
2084+ catch (RuntimeException | Error failure ) {
2085+ // SDK admission can fail before its startup-cleanup try/catch. Such an
2086+ // attempt admitted no new writers and must not strand an empty plugin owner.
2087+ // An admitted startup failure already entered our onDisable; preserve its drain.
2088+ if (!serverDataEnableCleanupStarted ) {
2089+ serverDataEnableAdmissionRejected = true ;
2090+ try {
2091+ serverDataPublicationFence .retire ();
2092+ serverDataFinalPublication .complete (null );
2093+ } catch (RuntimeException | Error cleanupFailure ) {
2094+ if (cleanupFailure != failure ) failure .addSuppressed (cleanupFailure );
2095+ }
2096+ }
2097+ throw failure ;
2098+ }
2099+ }
2100+
20142101 @ Override
20152102 public void onPreLoad () {
20162103 plugin = this ;
2104+ unloadInProgress .set (false );
2105+ completedUnloadSteps = new java .util .HashSet <>();
2106+ signShutdownPublication = null ;
20172107
20182108 setupFiles ();
20192109 if ("SQLITE" .equalsIgnoreCase (configFile .getData ().getString ("DataStorage" , "SQLITE" ))) {
@@ -2055,16 +2145,68 @@ public void onFinish() {
20552145
20562146 @ Override
20572147 public void onDisable () {
2058- if (discordHandler != null ) discordHandler .close ();
2059- stopAutoCreateVoteSitesSetupReminder ();
2060- if (offlineVoteOwnerHandoffs != null ) offlineVoteOwnerHandoffs .close ();
2148+ // The rejected enable owns no SDK shutdown stage to complete a new reservation.
2149+ if (serverDataEnableAdmissionRejected ) return ;
2150+ runShutdownStep ("server-data process drain" , this ::beginServerDataProcessDrain );
2151+ serverDataEnableCleanupStarted = true ;
2152+ runShutdownStep ("Discord leaderboards" , () -> {
2153+ if (discordHandler != null ) discordHandler .close ();
2154+ });
2155+ runShutdownStep ("setup reminder" , this ::stopAutoCreateVoteSitesSetupReminder );
2156+ runShutdownStep ("server-data writer retirement" , () -> {
2157+ if (serverDataPublicationFence != null ) {
2158+ retainServerDataFinalPublication ();
2159+ var coreWriters = getServerDataWriterExecutors ();
2160+ var writers = java .util .Arrays .copyOf (coreWriters , coreWriters .length + 1 );
2161+ writers [coreWriters .length ] = voteTimer ;
2162+ serverDataPublicationFence .retainUntilTermination (writers );
2163+ }
2164+ });
20612165 try {
2062- shutdownVoteReminders ();
2166+ runShutdownStep ("offline vote handoffs" , () -> {
2167+ if (offlineVoteOwnerHandoffs != null ) offlineVoteOwnerHandoffs .close ();
2168+ });
20632169 } finally {
2064- super .onDisable ();
2170+ try { shutdownVoteReminders (); }
2171+ finally {
2172+ try { super .onDisable (); }
2173+ finally { runShutdownStep ("server-data publication owner" , () -> {
2174+ if (serverDataPublicationFence != null ) serverDataPublicationFence .retire ();
2175+ }); }
2176+ }
20652177 }
20662178 }
20672179
2180+ /** Reserve before SDK shutdown can enqueue a final file publication. No YAML lock or wait. */
2181+ private void retainServerDataFinalPublication () {
2182+ synchronized (this ) {
2183+ if (serverDataFinalPublicationReserved ) return ;
2184+ if (serverDataFinalPublication == null )
2185+ serverDataFinalPublication = new java .util .concurrent .CompletableFuture <>();
2186+ serverDataPublicationFence .retainUntilSuccessfulPublication (serverDataFinalPublication );
2187+ serverDataFinalPublicationReserved = true ;
2188+ }
2189+ }
2190+
2191+ @ Override
2192+ public void onShutdownComplete (java .util .concurrent .CompletionStage <Void > finalFilesStage ) {
2193+ java .util .concurrent .CompletableFuture <Void > publication ;
2194+ synchronized (this ) {
2195+ publication = serverDataFinalStageAttached ? null : serverDataFinalPublication ;
2196+ serverDataFinalStageAttached = true ;
2197+ }
2198+ try {
2199+ if (publication != null ) {
2200+ try {
2201+ java .util .Objects .requireNonNull (finalFilesStage ).whenComplete ((ignored , failure ) -> {
2202+ if (failure == null ) publication .complete (null );
2203+ else publication .completeExceptionally (failure );
2204+ });
2205+ } catch (Throwable failure ) { publication .completeExceptionally (failure ); }
2206+ }
2207+ } finally { super .onShutdownComplete (finalFilesStage ); }
2208+ }
2209+
20682210 private Settings autoCreateVoteSitesReminderSettings () {
20692211 return new Settings (true , configFile .isAutoCreateVoteSites (),
20702212 AutoCreateVoteSitesReminderState .DEFAULT_THRESHOLD_MILLIS ,
@@ -2092,52 +2234,107 @@ private void shutdownVoteReminders() {
20922234
20932235 @ Override
20942236 public void onUnLoad () {
2095- if (discordHandler != null ) discordHandler .close ();
2096- retireTopVoterDisplayFilter ();
2097- if (voteStreaksMenu != null ) voteStreaksMenu .close ();
2098- stopAutoCreateVoteSitesSetupReminder ();
2099- placeholderPlayerPresence .clear ();
2100- stopBackendHostedControlLifecycle ();
2101- stopBackendControlConnectorLifecycle ();
2102- if (getBackendProxyHandler () != null ) {
2103- try {
2104- getBackendProxyHandler ().closeForShutdown ();
2105- } catch (Exception e ) {
2106- debug (e );
2107- }
2108- }
2109- if (backendOrderedVoteOverflowQueue != null ) {
2110- backendOrderedVoteOverflowQueue .close ();
2111- backendOrderedVoteOverflowQueue = null ;
2112- }
2113- if (webhookManager != null ) {
2114- webhookManager .shutdown ();
2237+ runShutdownStep ("server-data process drain" , this ::beginServerDataProcessDrain );
2238+ if (!unloadInProgress .compareAndSet (false , true )) {
2239+ runShutdownStep ("time queue retry" , () -> {
2240+ if (timeQueueHandler != null ) timeQueueHandler .saveForShutdown ();
2241+ });
2242+ return ;
21152243 }
2116- voteTimer .shutdown ();
21172244 try {
2118- voteTimer .awaitTermination (1 , TimeUnit .SECONDS );
2119- } catch (InterruptedException e ) {
2120- e .printStackTrace ();
2121- }
2122- voteTimer .shutdownNow ();
2123- if (votifierVoteOverflowQueue != null ) {
2124- votifierVoteOverflowQueue .close ();
2125- votifierVoteOverflowQueue = null ;
2126- }
2127- if (timeQueueHandler != null ) {
2128- timeQueueHandler .save ();
2129- }
2245+ if (completedUnloadSteps == null ) completedUnloadSteps = new java .util .HashSet <>();
2246+ runUnloadStep ("Discord leaderboards" , () -> {
2247+ if (discordHandler != null ) discordHandler .close ();
2248+ });
2249+ runUnloadStep ("top voter display" , this ::retireTopVoterDisplayFilter );
2250+ runUnloadStep ("streak menu" , () -> {
2251+ if (voteStreaksMenu != null ) voteStreaksMenu .close ();
2252+ });
2253+ runUnloadStep ("setup reminder" , this ::stopAutoCreateVoteSitesSetupReminder );
2254+ runUnloadStep ("player presence" , placeholderPlayerPresence ::clear );
2255+ runUnloadStep ("backend hosted Control" , this ::stopBackendHostedControlLifecycle );
2256+ runUnloadStep ("backend Control connector" , this ::stopBackendControlConnectorLifecycle );
2257+ runUnloadStep ("backend proxy" , () -> {
2258+ if (getBackendProxyHandler () != null ) getBackendProxyHandler ().closeForShutdown ();
2259+ });
2260+ runUnloadStep ("backend ordered vote queue" , () -> {
2261+ if (backendOrderedVoteOverflowQueue != null ) {
2262+ backendOrderedVoteOverflowQueue .close ();
2263+ backendOrderedVoteOverflowQueue = null ;
2264+ }
2265+ });
2266+ runUnloadStep ("webhook manager" , () -> {
2267+ if (webhookManager != null ) webhookManager .shutdown ();
2268+ });
2269+ runUnloadStep ("vote timer" , this ::shutdownVoteTimer );
2270+ runUnloadStep ("Votifier vote queue" , () -> {
2271+ if (votifierVoteOverflowQueue != null ) {
2272+ votifierVoteOverflowQueue .close ();
2273+ votifierVoteOverflowQueue = null ;
2274+ }
2275+ });
2276+ runShutdownStep ("time queue" , () -> {
2277+ if (timeQueueHandler != null ) timeQueueHandler .saveForShutdown ();
2278+ });
2279+ runUnloadStep ("vote reminders" , this ::shutdownVoteReminders );
2280+ runUnloadStep ("cooldown checker" , () -> {
2281+ if (coolDownCheck != null ) {
2282+ coolDownCheck .shutdown ();
2283+ coolDownCheck = null ;
2284+ }
2285+ });
2286+ runShutdownStep ("signs" , this ::shutdownSigns );
2287+ runUnloadStep ("event handlers" , () -> HandlerList .unregisterAll (this ));
2288+ // Late producers must see rejection until their overflow and handler cleanup finishes.
2289+ if (completedUnloadSteps .contains ("vote timer" )) voteTimer = null ;
2290+ if (plugin == this ) plugin = null ;
2291+ } finally { unloadInProgress .set (false ); }
2292+ }
2293+
2294+ private void beginServerDataProcessDrain () {
2295+ var owner = serverDataPublicationFence ;
2296+ if (owner != null ) owner .beginShutdown (System .nanoTime ());
2297+ }
21302298
2131- shutdownVoteReminders ();
2299+ private void shutdownSigns () {
2300+ if (signs == null ) return ;
2301+ var previous = signShutdownPublication ;
2302+ if (previous != null && !previous .toCompletableFuture ().isCompletedExceptionally ()) return ;
2303+ signShutdownPublication = java .util .Objects .requireNonNull (signs .storeSignsForShutdown ());
2304+ }
2305+
2306+ private void runUnloadStep (String name , Runnable step ) {
2307+ if (!completedUnloadSteps .contains (name ) && runShutdownStep (name , step )) completedUnloadSteps .add (name );
2308+ }
21322309
2133- if (coolDownCheck != null ) {
2134- coolDownCheck .shutdown ();
2135- coolDownCheck = null ;
2310+ private boolean runShutdownStep (String name , Runnable step ) {
2311+ try {
2312+ step .run ();
2313+ return true ;
2314+ } catch (RuntimeException | Error failure ) {
2315+ try {
2316+ getLogger ().log (java .util .logging .Level .WARNING , "Failed to shut down " + name , failure );
2317+ } catch (RuntimeException | Error ignored ) {
2318+ // Logging must not prevent the next independent cleanup step.
2319+ }
2320+ return false ;
21362321 }
2322+ }
21372323
2138- getSigns ().storeSigns ();
2139- HandlerList .unregisterAll (plugin );
2140- plugin = null ;
2324+ private void shutdownVoteTimer () {
2325+ if (voteTimer == null ) return ;
2326+ try {
2327+ voteTimer .shutdown ();
2328+ if (!voteTimer .awaitTermination (1 , TimeUnit .SECONDS )) voteTimer .shutdownNow ();
2329+ } catch (InterruptedException interrupted ) {
2330+ Thread .currentThread ().interrupt ();
2331+ voteTimer .shutdownNow ();
2332+ } catch (RuntimeException failure ) {
2333+ try {
2334+ getLogger ().log (java .util .logging .Level .WARNING , "Failed to shut down vote timer" , failure );
2335+ } catch (RuntimeException | Error ignored ) { }
2336+ voteTimer .shutdownNow ();
2337+ }
21412338 }
21422339
21432340 /**
0 commit comments