Skip to content

Commit aa07904

Browse files
authored
Merge pull request #58 from Dimotai/patch-1
Enhance warmup service with lifetime limit and error handling
2 parents 71f9077 + 8673f63 commit aa07904

1 file changed

Lines changed: 61 additions & 21 deletions

File tree

src/main/java/com/eliteessentials/services/WarmupService.java

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ public class WarmupService {
3030

3131
// Poll interval in milliseconds - frequent polling catches movement reliably
3232
private static final long POLL_INTERVAL_MS = 100;
33+
34+
// Maximum warmup lifetime in nanos - safety net to clear stuck entries
35+
// (e.g. world.execute() silently dropped the task, world was destroyed, etc.)
36+
private static final long MAX_WARMUP_LIFETIME_NANOS = TimeUnit.SECONDS.toNanos(60);
3337

3438
private final ScheduledExecutorService poller;
3539
private final Map<UUID, PendingWarmup> pending = new ConcurrentHashMap<>();
@@ -118,7 +122,10 @@ public void startWarmup(PlayerRef player, Vector3d startPosition, int warmupSeco
118122
}
119123

120124
private void ensurePollerRunning() {
121-
if (pollTask != null && !pollTask.isCancelled()) {
125+
// Check isDone() as well as isCancelled(): if the poller task threw an
126+
// uncaught exception, ScheduledExecutorService silently kills it.
127+
// isDone() returns true but isCancelled() returns false in that case.
128+
if (pollTask != null && !pollTask.isCancelled() && !pollTask.isDone()) {
122129
return;
123130
}
124131
pollTask = poller.scheduleAtFixedRate(
@@ -128,30 +135,61 @@ private void ensurePollerRunning() {
128135
}
129136

130137
private void pollWarmups() {
131-
if (pending.isEmpty()) {
132-
return;
133-
}
134-
135-
for (PendingWarmup warmup : pending.values()) {
136-
if (warmup.cancelled) {
137-
pending.remove(warmup.playerUuid);
138-
continue;
138+
// CRITICAL: This method runs inside ScheduledExecutorService.scheduleAtFixedRate().
139+
// If ANY uncaught exception escapes, the executor permanently and silently kills
140+
// the recurring task. The entire warmup system goes dead — no player can teleport.
141+
// Every code path must be wrapped in try/catch.
142+
try {
143+
if (pending.isEmpty()) {
144+
return;
139145
}
140146

141-
World world = warmup.world;
142-
if (world == null) {
143-
pending.remove(warmup.playerUuid);
144-
continue;
147+
long now = System.nanoTime();
148+
149+
for (PendingWarmup warmup : pending.values()) {
150+
try {
151+
if (warmup.cancelled) {
152+
pending.remove(warmup.playerUuid);
153+
continue;
154+
}
155+
156+
// Safety net: clear warmups that have been pending far too long.
157+
// This catches cases where world.execute() silently drops the task,
158+
// the world was destroyed but the reference is non-null, etc.
159+
if ((now - warmup.createdAtNanos) > MAX_WARMUP_LIFETIME_NANOS) {
160+
logger.warning("[Warmup] Clearing stale warmup for " + warmup.playerUuid
161+
+ " (" + warmup.commandName + ") - exceeded max lifetime");
162+
pending.remove(warmup.playerUuid);
163+
continue;
164+
}
165+
166+
World world = warmup.world;
167+
if (world == null) {
168+
pending.remove(warmup.playerUuid);
169+
continue;
170+
}
171+
172+
// Execute the tick on the game thread
173+
world.execute(() -> tickWarmup(warmup));
174+
} catch (Exception e) {
175+
// Per-warmup catch: one bad warmup must never kill processing for others.
176+
// This typically fires when world.execute() throws because the world
177+
// was destroyed between the null check and the execute call.
178+
logger.warning("[Warmup] Error polling warmup for " + warmup.playerUuid
179+
+ " (" + warmup.commandName + "): " + e.getMessage() + " - removing");
180+
pending.remove(warmup.playerUuid);
181+
}
145182
}
146183

147-
// Execute the tick on the game thread
148-
world.execute(() -> tickWarmup(warmup));
149-
}
150-
151-
// Stop poller if no more pending warmups
152-
if (pending.isEmpty() && pollTask != null) {
153-
pollTask.cancel(false);
154-
pollTask = null;
184+
// Stop poller if no more pending warmups
185+
if (pending.isEmpty() && pollTask != null) {
186+
pollTask.cancel(false);
187+
pollTask = null;
188+
}
189+
} catch (Exception e) {
190+
// Outer catch: absolute last resort. If we somehow get here, log it
191+
// but do NOT let the exception propagate or the poller dies permanently.
192+
logger.severe("[Warmup] Unexpected error in pollWarmups: " + e.getMessage());
155193
}
156194
}
157195

@@ -287,6 +325,7 @@ private static class PendingWarmup {
287325
final Ref<EntityStore> playerRef;
288326
final Vector3d startPos;
289327
final long endTimeNanos;
328+
final long createdAtNanos;
290329
final Runnable onComplete;
291330
final String commandName;
292331
final World world;
@@ -302,6 +341,7 @@ private static class PendingWarmup {
302341
this.playerRef = playerRef;
303342
this.startPos = startPos;
304343
this.endTimeNanos = endTimeNanos;
344+
this.createdAtNanos = System.nanoTime();
305345
this.onComplete = onComplete;
306346
this.commandName = commandName;
307347
this.world = world;

0 commit comments

Comments
 (0)