From 87d320c1f4abd08ccc82a0b3262e6e9f73bbbf5a Mon Sep 17 00:00:00 2001 From: laforetbrut Date: Thu, 26 Mar 2026 11:51:09 +0100 Subject: [PATCH] Fix excessive thread creation (issue #169) - bounded thread pool Replace unbounded CachedThreadPool with bounded ThreadPoolExecutor. Problem: CachedThreadPool creates unlimited threads. With many players online and slow DB queries, thread count explodes (25000+ threads observed in issue #169), causing memory leaks and server crashes. Fix: ThreadPoolExecutor with 2 core / 8 max threads, 30s keepalive, 256-task bounded queue, and CallerRunsPolicy for backpressure. When the queue is full, tasks execute on the calling thread instead of creating more threads, providing natural flow control. Closes mlus-asuka/PlayerSync#169 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../fubuki/playersync/sync/VanillaSync.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java b/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java index 5400937..e370437 100644 --- a/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java +++ b/src/main/java/vip/fubuki/playersync/sync/VanillaSync.java @@ -57,9 +57,7 @@ import java.nio.file.Path; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.*; import java.util.concurrent.locks.ReentrantLock; @EventBusSubscriber(modid = PlayerSync.MODID) @@ -67,7 +65,20 @@ public class VanillaSync { public static void register() {} - static ExecutorService executorService = Executors.newCachedThreadPool(new PSThreadPoolFactory("PlayerSync")); + // FIX: Replace unbounded CachedThreadPool with a bounded ThreadPoolExecutor. + // CachedThreadPool creates unlimited threads — with many players and slow DB queries, + // thread count can explode to 25000+ causing memory leaks and server crashes. + // Bounded pool: 2 core threads, max 8 threads, 30s keepalive, 256-task queue. + // If the queue is full, tasks run on the calling thread (CallerRunsPolicy) which + // provides natural backpressure instead of creating more threads. + static ExecutorService executorService = new ThreadPoolExecutor( + 2, // core pool size + 8, // maximum pool size + 30L, TimeUnit.SECONDS, // idle thread keepalive + new LinkedBlockingQueue<>(256), // bounded work queue + new PSThreadPoolFactory("PlayerSync"), + new ThreadPoolExecutor.CallerRunsPolicy() // backpressure: run on caller thread if queue full + ); // Per-player locks to prevent concurrent save/restore operations (anti-duplication) private static final ConcurrentHashMap playerLocks = new ConcurrentHashMap<>();