Skip to content

Commit b5944db

Browse files
committed
Fix Discord Link RoleSyncManager thread leak
Two issues causing unbounded thread growth: 1. The repeating sync task was never cancelled on plugin disable, leaking the timer itself. 2. Each sync() call spawns an async task that calls acquireUninterruptibly() on a 5-permit semaphore. The timer fires up to 50 sync calls per cycle, so 45 threads block indefinitely waiting for permits. Before they drain, the next cycle spawns 50 more. Threads accumulate until OOM. Fix: cancel the task on disable, and replace acquireUninterruptibly() with tryAcquire(5s timeout) in both sync() and unSync() so threads don't block indefinitely — skipped syncs retry on the next cycle. Fixes #6381
1 parent 63e7c4d commit b5944db

File tree

2 files changed

+27
-4
lines changed

2 files changed

+27
-4
lines changed

EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/EssentialsDiscordLink.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ public void onEnable() {
8787

8888
@Override
8989
public void onDisable() {
90+
if (roleSyncManager != null) {
91+
roleSyncManager.shutdown();
92+
}
9093
if (accounts != null) {
9194
accounts.shutdown();
9295
}

EssentialsDiscordLink/src/main/java/net/essentialsx/discordlink/rolesync/RoleSyncManager.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import net.essentialsx.discordlink.EssentialsDiscordLink;
88
import org.bukkit.Bukkit;
99
import org.bukkit.entity.Player;
10+
import org.bukkit.scheduler.BukkitTask;
1011
import org.bukkit.event.EventHandler;
1112
import org.bukkit.event.Listener;
1213
import org.bukkit.event.player.PlayerJoinEvent;
@@ -19,6 +20,7 @@
1920
import java.util.UUID;
2021
import java.util.concurrent.CompletableFuture;
2122
import java.util.concurrent.Semaphore;
23+
import java.util.concurrent.TimeUnit;
2224
import java.util.logging.Level;
2325

2426
import static com.earth2me.essentials.I18n.tlLiteral;
@@ -28,13 +30,14 @@ public class RoleSyncManager implements Listener {
2830
private final Map<String, InteractionRole> groupToRoleMap = new HashMap<>();
2931
private final Map<String, String> roleIdToGroupMap = new HashMap<>();
3032
private final Semaphore syncSemaphore = new Semaphore(5);
33+
private BukkitTask syncTask;
3134
private int syncCursor = 0;
3235

3336
public RoleSyncManager(final EssentialsDiscordLink ess) {
3437
this.ess = ess;
3538
Bukkit.getPluginManager().registerEvents(this, ess);
3639
onReload();
37-
this.ess.getEss().runTaskTimerAsynchronously(() -> {
40+
this.syncTask = this.ess.getEss().runTaskTimerAsynchronously(() -> {
3841
if (groupToRoleMap.isEmpty() && roleIdToGroupMap.isEmpty()) {
3942
return;
4043
}
@@ -67,6 +70,12 @@ public RoleSyncManager(final EssentialsDiscordLink ess) {
6770
}, 0, ess.getSettings().getRoleSyncResyncDelay() * 1200L);
6871
}
6972

73+
public void shutdown() {
74+
if (syncTask != null) {
75+
syncTask.cancel();
76+
}
77+
}
78+
7079
public void sync(final UUID uuid, final String discordId) {
7180
final Map<String, InteractionRole> groupToRoleMapCopy = new HashMap<>(groupToRoleMap);
7281
final Map<String, String> roleIdToGroupMapCopy = new HashMap<>(roleIdToGroupMap);
@@ -81,7 +90,13 @@ public void sync(final Player player, final String discordId, final Map<String,
8190
final List<String> groups = primaryOnly ?
8291
Collections.singletonList(ess.getEss().getPermissionsHandler().getGroup(player)) : ess.getEss().getPermissionsHandler().getGroups(player);
8392
ess.getEss().runTaskAsynchronously(() -> {
84-
syncSemaphore.acquireUninterruptibly();
93+
try {
94+
if (!syncSemaphore.tryAcquire(5, TimeUnit.SECONDS)) {
95+
return;
96+
}
97+
} catch (final InterruptedException e) {
98+
return;
99+
}
85100
ess.getApi().getMemberById(discordId).thenCompose(member -> {
86101
if (member == null) {
87102
if (ess.getSettings().isUnlinkOnLeave()) {
@@ -151,9 +166,14 @@ public void unSync(final UUID uuid, final String discordId) {
151166
}
152167

153168
ess.getEss().runTaskAsynchronously(() -> {
154-
syncSemaphore.acquireUninterruptibly();
169+
try {
170+
if (!syncSemaphore.tryAcquire(5, TimeUnit.SECONDS)) {
171+
return;
172+
}
173+
} catch (final InterruptedException e) {
174+
return;
175+
}
155176
ess.getApi().getMemberById(discordId).thenCompose(member -> {
156-
// Check if the member is no longer in the guild (null), they don't have any roles anyway.
157177
if (member == null) {
158178
return CompletableFuture.completedFuture(null);
159179
}

0 commit comments

Comments
 (0)