Skip to content

Commit 2a1e4e1

Browse files
authored
Merge pull request #1701 from BenCodez/fix/partial-startup-shutdown
Prevent shutdown exceptions after partial startup failures
2 parents c80e460 + 73947ee commit 2a1e4e1

37 files changed

Lines changed: 6367 additions & 262 deletions

‎VotingPlugin/src/main/java/com/bencodez/votingplugin/VotingPluginMain.java‎

Lines changed: 242 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)