Add channel auto locking, implement thread archiving. More fixes to shutdown not working properly/ errors when the server is stopped before DiscordSRV starts

This commit is contained in:
Vankka 2022-04-02 16:24:34 +03:00
parent 51fd9ef6d8
commit bbc722e689
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
13 changed files with 325 additions and 53 deletions

View File

@ -39,6 +39,10 @@ public interface Module {
return 0;
}
default int shutdownOrder() {
return priority(getClass());
}
default void enable() {
reload();
}

View File

@ -26,6 +26,7 @@ import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent;
import com.discordsrv.api.module.type.Module;
import com.discordsrv.common.api.util.ApiInstanceUtil;
import com.discordsrv.common.channel.ChannelConfigHelper;
import com.discordsrv.common.channel.ChannelShutdownBehaviourModule;
import com.discordsrv.common.channel.ChannelUpdaterModule;
import com.discordsrv.common.channel.GlobalChannelLookupModule;
import com.discordsrv.common.command.game.GameCommandModule;
@ -388,7 +389,7 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
// Lifecycle
protected CompletableFuture<Void> invokeLifecycle(CheckedRunnable runnable) {
return invoke(() -> {
return invokeLifecycle(() -> {
try {
lifecycleLock.lock();
runnable.run();
@ -398,11 +399,19 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
}, "Failed to enable", true);
}
protected CompletableFuture<Void> invoke(CheckedRunnable runnable, String message, boolean enable) {
protected CompletableFuture<Void> invokeLifecycle(CheckedRunnable runnable, String message, boolean enable) {
return CompletableFuture.runAsync(() -> {
if (status().isShutdown()) {
// Already shutdown/shutting down, don't bother
return;
}
try {
runnable.run();
} catch (Throwable t) {
if (status().isShutdown() && t instanceof NoClassDefFoundError) {
// Already shutdown, ignore errors for classes that already got unloaded
return;
}
if (enable) {
setStatus(Status.FAILED_TO_START);
disable();
@ -424,6 +433,7 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
@Override
public final CompletableFuture<Void> invokeDisable() {
if (enableFuture != null && !enableFuture.isDone()) {
logger().warning("Start cancelled");
enableFuture.cancel(true);
}
return CompletableFuture.runAsync(this::disable, scheduler().executorService());
@ -431,7 +441,7 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
@Override
public final CompletableFuture<Void> invokeReload(Set<ReloadFlag> flags, boolean silent) {
return invoke(() -> reload(flags, silent), "Failed to reload", false);
return invokeLifecycle(() -> reload(flags, silent), "Failed to reload", false);
}
@OverridingMethodsMustInvokeSuper
@ -453,6 +463,7 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
placeholderService().addGlobalContext(new GlobalTextHandlingContext(this));
// Modules
registerModule(ChannelShutdownBehaviourModule::new);
registerModule(ChannelUpdaterModule::new);
registerModule(GameCommandModule::new);
registerModule(GlobalChannelLookupModule::new);
@ -588,6 +599,16 @@ public abstract class AbstractDiscordSRV<C extends MainConfig, CC extends Connec
}
if (!initial) {
waitForStatus(Status.CONNECTED, 20, TimeUnit.SECONDS);
} else {
JDA jda = jda().orElse(null);
if (jda != null) {
try {
jda.awaitReady();
} catch (IllegalStateException ignored) {
// JDA shutdown -> don't continue
return;
}
}
}
} catch (ExecutionException e) {
throw e.getCause();

View File

@ -0,0 +1,161 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.channel;
import com.discordsrv.api.discord.api.entity.channel.DiscordThreadChannel;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.config.main.channels.ShutdownBehaviourConfig;
import com.discordsrv.common.config.main.channels.base.BaseChannelConfig;
import com.discordsrv.common.config.main.channels.base.IChannelConfig;
import com.discordsrv.common.function.OrDefault;
import com.discordsrv.common.module.type.AbstractModule;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.IPermissionHolder;
import net.dv8tion.jda.api.entities.PermissionOverride;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.requests.restaction.PermissionOverrideAction;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.BiConsumer;
public class ChannelShutdownBehaviourModule extends AbstractModule<DiscordSRV> {
public ChannelShutdownBehaviourModule(DiscordSRV discordSRV) {
super(discordSRV);
}
@Override
public int shutdownOrder() {
return Integer.MIN_VALUE;
}
@Override
public void enable() {
doForAllChannels((config, channelConfig) -> {
OrDefault<ShutdownBehaviourConfig> shutdownConfig = config.map(cfg -> cfg.shutdownBehaviour);
OrDefault<ShutdownBehaviourConfig.Channels> channels = shutdownConfig.map(cfg -> cfg.channels);
OrDefault<ShutdownBehaviourConfig.Threads> threads = shutdownConfig.map(cfg -> cfg.threads);
if (threads.get(cfg -> cfg.unarchive, true)) {
discordSRV.discordAPI().findOrCreateThreads(config, channelConfig, __ -> {}, new ArrayList<>(), false);
}
channelPermissions(channelConfig, channels, true);
});
}
@Override
public void disable() {
doForAllChannels((config, channelConfig) -> {
OrDefault<ShutdownBehaviourConfig> shutdownConfig = config.map(cfg -> cfg.shutdownBehaviour);
OrDefault<ShutdownBehaviourConfig.Channels> channels = shutdownConfig.map(cfg -> cfg.channels);
OrDefault<ShutdownBehaviourConfig.Threads> threads = shutdownConfig.map(cfg -> cfg.threads);
if (threads.get(cfg -> cfg.archive, true)) {
for (DiscordThreadChannel thread : discordSRV.discordAPI().findThreads(config, channelConfig)) {
thread.getAsJDAThreadChannel().getManager()
.setArchived(true)
.reason("DiscordSRV shutdown behaviour")
.queue();
}
}
channelPermissions(channelConfig, channels, false);
});
}
private void channelPermissions(
IChannelConfig channelConfig,
OrDefault<ShutdownBehaviourConfig.Channels> shutdownConfig,
boolean state
) {
JDA jda = discordSRV.jda().orElse(null);
if (jda == null) {
return;
}
boolean everyone = shutdownConfig.get(cfg -> cfg.everyone, false);
List<Long> roleIds = shutdownConfig.get(cfg -> cfg.roleIds, Collections.emptyList());
if (!everyone && roleIds.isEmpty()) {
return;
}
List<Permission> permissions = new ArrayList<>();
if (shutdownConfig.get(cfg -> cfg.read, false)) {
permissions.add(Permission.VIEW_CHANNEL);
}
if (shutdownConfig.get(cfg -> cfg.write, true)) {
permissions.add(Permission.MESSAGE_SEND);
}
if (shutdownConfig.get(cfg -> cfg.addReactions, true)) {
permissions.add(Permission.MESSAGE_ADD_REACTION);
}
for (Long channelId : channelConfig.channelIds()) {
TextChannel channel = jda.getTextChannelById(channelId);
if (channel == null) {
continue;
}
if (everyone) {
setPermission(channel, channel.getGuild().getPublicRole(), permissions, state);
}
for (Long roleId : roleIds) {
Role role = channel.getGuild().getRoleById(roleId);
if (role == null) {
continue;
}
setPermission(channel, role, permissions, state);
}
}
}
private void setPermission(TextChannel channel, IPermissionHolder holder, List<Permission> permissions, boolean state) {
PermissionOverride override = channel.getPermissionOverride(holder);
if (override != null && (state ? override.getAllowed() : override.getDenied()).containsAll(permissions)) {
// Already correct
return;
}
PermissionOverrideAction action = override != null
? override.getManager()
: channel.putPermissionOverride(holder);
if (state) {
action = action.grant(permissions);
} else {
action = action.deny(permissions);
}
action.reason("DiscordSRV shutdown behaviour").queue();
}
private void doForAllChannels(BiConsumer<OrDefault<BaseChannelConfig>, IChannelConfig> channelConsumer) {
for (OrDefault<BaseChannelConfig> config : discordSRV.channelConfig().getAllChannels()) {
IChannelConfig channelConfig = config.get(cfg -> cfg instanceof IChannelConfig ? (IChannelConfig) cfg : null);
if (channelConfig == null) {
continue;
}
channelConsumer.accept(config, channelConfig);
}
}
}

View File

@ -0,0 +1,59 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.config.main.channels;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import java.util.ArrayList;
import java.util.List;
@ConfigSerializable
public class ShutdownBehaviourConfig {
public Channels channels = new Channels();
public Threads threads = new Threads();
@ConfigSerializable
public static class Channels {
@Comment("If the permissions should be taken from @everyone while the server is offline")
public boolean everyone = false;
@Comment("Role ids for roles that should have the permissions taken while the server is offline")
public List<Long> roleIds = new ArrayList<>();
public boolean read = false;
public boolean write = true;
public boolean addReactions = true;
}
@ConfigSerializable
public static class Threads {
@Comment("If threads should be archived while the server is shutdown")
public boolean archive = true;
@Comment("If the bot will attempt to unarchive threads rather than make new threads")
public boolean unarchive = true;
}
}

View File

@ -31,14 +31,20 @@ public class BaseChannelConfig {
public DiscordToMinecraftChatConfig discordToMinecraft = new DiscordToMinecraftChatConfig();
public JoinMessageConfig joinMessages = new JoinMessageConfig();
public LeaveMessageConfig leaveMessages = new LeaveMessageConfig();
@Untranslated(Untranslated.Type.VALUE)
@Order(10)
public String avatarUrlProvider = "https://heads.discordsrv.com/head.png?texture=%texture%&uuid=%uuid%&name=%username%&overlay";
@Order(20)
public StartMessageConfig startMessage = new StartMessageConfig();
@Order(20)
public StopMessageConfig stopMessage = new StopMessageConfig();
@Order(10)
@Order(30)
@Comment("Settings for synchronizing messages between the defined Discord channels and threads")
public MirroringConfig mirroring = new MirroringConfig();
@Untranslated(Untranslated.Type.VALUE)
@Order(50)
public String avatarUrlProvider = "https://heads.discordsrv.com/head.png?texture=%texture%&uuid=%uuid%&name=%username%&overlay";
public ShutdownBehaviourConfig shutdownBehaviour = new ShutdownBehaviourConfig();
}

View File

@ -28,6 +28,4 @@ public class ThreadConfig {
public String threadName = "Minecraft Server chat bridge";
public boolean privateThread = false;
public boolean archiveOnShutdown = true;
public boolean unarchive = true;
}

View File

@ -42,6 +42,7 @@ import com.discordsrv.common.discord.api.entity.channel.DiscordThreadChannelImpl
import com.discordsrv.common.discord.api.entity.guild.DiscordGuildImpl;
import com.discordsrv.common.discord.api.entity.guild.DiscordRoleImpl;
import com.discordsrv.common.function.CheckedSupplier;
import com.discordsrv.common.function.OrDefault;
import com.discordsrv.common.future.util.CompletableFutureUtil;
import com.github.benmanes.caffeine.cache.AsyncCacheLoader;
import com.github.benmanes.caffeine.cache.AsyncLoadingCache;
@ -98,9 +99,9 @@ public class DiscordAPIImpl implements DiscordAPI {
* @param config the config that specified the threads
* @return the list of active threads
*/
public List<DiscordThreadChannel> findThreads(IChannelConfig config) {
public List<DiscordThreadChannel> findThreads(OrDefault<BaseChannelConfig> config, IChannelConfig channelConfig) {
List<DiscordThreadChannel> channels = new ArrayList<>();
findOrCreateThreads(config, channels::add, null);
findOrCreateThreads(config, channelConfig, channels::add, null, false);
return channels;
}
@ -111,11 +112,13 @@ public class DiscordAPIImpl implements DiscordAPI {
* @param futures a possibly null list of {@link CompletableFuture} for tasks that need to be completed to get all threads
*/
public void findOrCreateThreads(
IChannelConfig config,
OrDefault<BaseChannelConfig> config,
IChannelConfig channelConfig,
Consumer<DiscordThreadChannel> channelConsumer,
@Nullable List<CompletableFuture<DiscordThreadChannel>> futures
@Nullable List<CompletableFuture<DiscordThreadChannel>> futures,
boolean log
) {
List<ThreadConfig> threads = config.threads();
List<ThreadConfig> threads = channelConfig.threads();
if (threads == null) {
return;
}
@ -124,10 +127,8 @@ public class DiscordAPIImpl implements DiscordAPI {
long channelId = threadConfig.channelId;
DiscordTextChannel channel = getTextChannelById(channelId).orElse(null);
if (channel == null) {
if (channelId > 0) {
discordSRV.logger().error("Unable to find channel with ID "
+ Long.toUnsignedString(channelId)
+ ", unable to forward message to Discord");
if (channelId > 0 && log) {
discordSRV.logger().error("Unable to find channel with ID " + Long.toUnsignedString(channelId));
}
continue;
}
@ -136,7 +137,7 @@ public class DiscordAPIImpl implements DiscordAPI {
DiscordThreadChannel thread = findThread(threadConfig, channel.getActiveThreads());
if (thread != null) {
ThreadChannel jdaChannel = thread.getAsJDAThreadChannel();
if (!jdaChannel.isLocked() && !jdaChannel.isArchived()) {
if (!jdaChannel.isArchived()) {
channelConsumer.accept(thread);
continue;
}
@ -154,7 +155,7 @@ public class DiscordAPIImpl implements DiscordAPI {
unarchiveOrCreateThread(threadConfig, channel, thread, future);
} else {
// Find or create the thread
future = findOrCreateThread(threadConfig, channel);
future = findOrCreateThread(config, threadConfig, channel);
}
futures.add(future.handle((threadChannel, t) -> {
@ -182,16 +183,20 @@ public class DiscordAPIImpl implements DiscordAPI {
return null;
}
private CompletableFuture<DiscordThreadChannel> findOrCreateThread(ThreadConfig config, DiscordTextChannel textChannel) {
if (!config.unarchive) {
return textChannel.createThread(config.threadName, config.privateThread);
private CompletableFuture<DiscordThreadChannel> findOrCreateThread(
OrDefault<BaseChannelConfig> config,
ThreadConfig threadConfig,
DiscordTextChannel textChannel
) {
if (!config.map(cfg -> cfg.shutdownBehaviour).map(cfg -> cfg.threads).get(cfg -> cfg.unarchive, true)) {
return textChannel.createThread(threadConfig.threadName, threadConfig.privateThread);
}
CompletableFuture<DiscordThreadChannel> completableFuture = new CompletableFuture<>();
lookupThreads(
textChannel,
config.privateThread,
lookup -> findOrCreateThread(config, textChannel, lookup, completableFuture),
threadConfig.privateThread,
lookup -> findOrCreateThread(threadConfig, textChannel, lookup, completableFuture),
(thread, throwable) -> {
if (throwable != null) {
completableFuture.completeExceptionally(throwable);
@ -244,10 +249,10 @@ public class DiscordAPIImpl implements DiscordAPI {
ThreadChannel channel = thread.getAsJDAThreadChannel();
if (channel.isLocked() || channel.isArchived()) {
try {
channel.getManager().setArchived(false).queue(
v -> future.complete(thread),
future::completeExceptionally
);
channel.getManager()
.setArchived(false)
.reason("DiscordSRV Auto Unarchive")
.queue(v -> future.complete(thread), future::completeExceptionally);
} catch (Throwable t) {
future.completeExceptionally(t);
}

View File

@ -253,7 +253,7 @@ public class JDAConnectionManager implements DiscordConnectionManager {
// Our own (named) threads
jdaBuilder.setCallbackPool(discordSRV.scheduler().forkJoinPool());
jdaBuilder.setGatewayPool(gatewayPool);
jdaBuilder.setRateLimitPool(rateLimitPool);
jdaBuilder.setRateLimitPool(rateLimitPool, true);
OkHttpClient.Builder httpBuilder = IOUtil.newHttpClientBuilder();
// These 3 are 10 seconds by default
@ -304,17 +304,20 @@ public class JDAConnectionManager implements DiscordConnectionManager {
instance.shutdown();
try {
discordSRV.logger().info("Waiting up to " + TimeUnit.MILLISECONDS.toSeconds(timeoutMillis) + " seconds for JDA to shutdown...");
discordSRV.scheduler().run(() -> {
try {
while (instance != null && instance.getStatus() != JDA.Status.SHUTDOWN) {
while (instance != null && !rateLimitPool.isShutdown()) {
Thread.sleep(50);
}
} catch (InterruptedException ignored) {}
}).get(timeoutMillis, TimeUnit.MILLISECONDS);
instance = null;
shutdownExecutors();
discordSRV.logger().info("JDA shutdown completed.");
} catch (TimeoutException | ExecutionException e) {
try {
discordSRV.logger().info("JDA failed to shutdown within the timeout, cancelling any remaining requests");
shutdownNow();
} catch (Throwable t) {
if (e instanceof ExecutionException) {
@ -332,13 +335,14 @@ public class JDAConnectionManager implements DiscordConnectionManager {
instance = null;
}
shutdownExecutors();
discordSRV.logger().info("JDA shutdown completed.");
}
private void shutdownExecutors() {
if (gatewayPool != null) {
gatewayPool.shutdownNow();
}
if (rateLimitPool != null) {
if (rateLimitPool != null && !rateLimitPool.isShutdown()) {
rateLimitPool.shutdownNow();
}
}

View File

@ -19,12 +19,13 @@
package com.discordsrv.common.logging.impl;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.logging.LogLevel;
import com.discordsrv.common.logging.LogAppender;
import com.discordsrv.common.logging.LogLevel;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.RejectedExecutionException;
public class DependencyLoggingHandler implements LogAppender {
@ -89,6 +90,14 @@ public class DependencyLoggingHandler implements LogAppender {
}
}
if (name.equals("JDA") && message != null
&& message.contains("Got an unexpected error. Please redirect the following message to the devs:")
&& throwable instanceof RejectedExecutionException
&& discordSRV.status().isShutdown()) {
// Might happen if the server shuts down while JDA is starting
return;
}
discordSRV.logger().log(null, logLevel, "[" + name + "]" + (message != null ? " " + message : ""), throwable);
}
}

View File

@ -162,8 +162,13 @@ public class DiscordSRVLogger implements Logger {
Files.write(path, line.getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND);
} catch (Throwable e) {
try {
// Prevent infinite loop
if (discordSRV.status() == DiscordSRV.Status.SHUTDOWN) {
return;
}
discordSRV.platformLogger().error("Failed to write to debug log", e);
} catch (Throwable ignored) {}
}
}

View File

@ -104,11 +104,11 @@ public class DiscordMessageMirroringModule extends AbstractModule<DiscordSRV> {
}
}
discordSRV.discordAPI().findOrCreateThreads(iChannelConfig, threadChannel -> {
discordSRV.discordAPI().findOrCreateThreads(channelConfig, iChannelConfig, threadChannel -> {
if (threadChannel.getId() != channel.getId()) {
mirrorChannels.add(Pair.of(threadChannel, config));
}
}, futures);
}, futures, false);
}
CompletableFutureUtil.combine(futures).whenComplete((v, t) -> {

View File

@ -78,24 +78,24 @@ public abstract class AbstractGameMessageModule<T extends IMessageConfig> extend
private CompletableFuture<Void> forwardToChannel(
@Nullable AbstractGameMessageReceiveEvent event,
@Nullable DiscordSRVPlayer player,
@NotNull OrDefault<BaseChannelConfig> channelConfig
@NotNull OrDefault<BaseChannelConfig> config
) {
OrDefault<T> config = mapConfig(channelConfig);
if (!config.get(IMessageConfig::enabled, true)) {
OrDefault<T> moduleConfig = mapConfig(config);
if (!moduleConfig.get(IMessageConfig::enabled, true)) {
return null;
}
IChannelConfig iChannelConfig = channelConfig.get(cfg -> cfg instanceof IChannelConfig ? (IChannelConfig) cfg : null);
if (iChannelConfig == null) {
IChannelConfig channelConfig = config.get(c -> c instanceof IChannelConfig ? (IChannelConfig) c : null);
if (channelConfig == null) {
return null;
}
List<DiscordMessageChannel> messageChannels = new CopyOnWriteArrayList<>();
List<CompletableFuture<DiscordThreadChannel>> futures = new ArrayList<>();
List<Long> channelIds = iChannelConfig.channelIds();
List<Long> channelIds = channelConfig.channelIds();
if (channelIds != null) {
for (Long channelId : iChannelConfig.channelIds()) {
for (Long channelId : channelConfig.channelIds()) {
DiscordTextChannel textChannel = discordSRV.discordAPI().getTextChannelById(channelId).orElse(null);
if (textChannel != null) {
messageChannels.add(textChannel);
@ -107,24 +107,24 @@ public abstract class AbstractGameMessageModule<T extends IMessageConfig> extend
}
}
discordSRV.discordAPI().findOrCreateThreads(iChannelConfig, messageChannels::add, futures);
discordSRV.discordAPI().findOrCreateThreads(config, channelConfig, messageChannels::add, futures, true);
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).whenComplete((v, t1) -> {
SendableDiscordMessage.Builder format = config.get(IMessageConfig::format);
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenCompose((v) -> {
SendableDiscordMessage.Builder format = moduleConfig.get(IMessageConfig::format);
if (format == null) {
return;
return CompletableFuture.completedFuture(null);
}
Component component = event != null ? ComponentUtil.fromAPI(event.getMessage()) : null;
String message = component != null ? convertMessage(config, component) : null;
String message = component != null ? convertMessage(moduleConfig, component) : null;
Map<CompletableFuture<ReceivedDiscordMessage>, DiscordMessageChannel> messageFutures;
messageFutures = sendMessageToChannels(
config, format, messageChannels, message,
moduleConfig, format, messageChannels, message,
// Context
channelConfig, player
);
CompletableFuture.allOf(messageFutures.keySet().toArray(new CompletableFuture[0]))
return CompletableFuture.allOf(messageFutures.keySet().toArray(new CompletableFuture[0]))
.whenComplete((vo, t2) -> {
Set<ReceivedDiscordMessage> messages = new LinkedHashSet<>();
for (Map.Entry<CompletableFuture<ReceivedDiscordMessage>, DiscordMessageChannel> entry : messageFutures.entrySet()) {

View File

@ -142,9 +142,9 @@ public class ModuleManager {
@Subscribe(priority = EventPriority.EARLY)
public void onShuttingDown(DiscordSRVShuttingDownEvent event) {
for (Module module : modules) {
unregister(module);
}
modules.stream()
.sorted((m1, m2) -> Integer.compare(m2.shutdownOrder(), m1.shutdownOrder()))
.forEachOrdered(Module::disable);
}
public void reload() {