Skip to content

Commit a036a0a

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 a036a0a

2 files changed

Lines changed: 27 additions & 3 deletions

File tree

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 & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.bukkit.event.EventHandler;
1111
import org.bukkit.event.Listener;
1212
import org.bukkit.event.player.PlayerJoinEvent;
13+
import org.bukkit.scheduler.BukkitTask;
1314

1415
import java.util.ArrayList;
1516
import java.util.Collections;
@@ -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,7 +166,13 @@ 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 -> {
156177
// Check if the member is no longer in the guild (null), they don't have any roles anyway.
157178
if (member == null) {

0 commit comments

Comments
 (0)