mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-09-11 11:45:27 +02:00
Release the background resources a discarded runtime owns
Several resources outlived a disconnect on purpose, which was harmless while a process ran exactly one session for its whole life. They are now released when the controller itself is closed: - the ON4KST connection supervisor thread, which stopByUser did not touch - the sked reminder scheduler, which had no shutdown at all - the reachability executor, whose shutdown method existed but was never called - the PSTRotator retry scheduler and its pending retry - the map tile proxy, whose stop method existed but was never called, leaving a server socket and a twelve thread pool behind - the station map bridge listeners and its coalescing animation, which would otherwise keep firing into a dead user interface These are real leaks today; they only become visible when a second runtime is built in the same process. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hpa6bjie5qkeNG62y6FmXm
This commit is contained in:
@@ -1083,9 +1083,35 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
rotatorClient.stop();
|
||||
rotatorClient = null;
|
||||
}
|
||||
|
||||
releaseBackgroundExecutors();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the background executors that live as long as this controller.
|
||||
*
|
||||
* <p>These are not bound to one ON4KST session, so disconnecting leaves them running
|
||||
* on purpose. When the controller itself is discarded they have to go, otherwise a
|
||||
* discarded controller stays reachable through its own threads.</p>
|
||||
*/
|
||||
private void releaseBackgroundExecutors() {
|
||||
|
||||
on4KstConnectionManager.shutdown();
|
||||
skedReminderService.shutdown();
|
||||
|
||||
if (reachabilityService != null) {
|
||||
reachabilityService.shutdown();
|
||||
}
|
||||
|
||||
if (pendingRotatorRetry != null) {
|
||||
pendingRotatorRetry.cancel(false);
|
||||
pendingRotatorRetry = null;
|
||||
}
|
||||
|
||||
rotatorCommandScheduler.shutdownNow();
|
||||
}
|
||||
|
||||
private void cancelTimer(Timer timer) {
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
|
||||
@@ -139,6 +139,20 @@ final class On4KstConnectionManager {
|
||||
scheduler.execute(() -> openConnection(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the supervisor thread of this manager for good.
|
||||
*
|
||||
* <p>{@link #stopByUser()} only ends the current ON4KST session; the periodic
|
||||
* session monitor keeps running. That is correct while the application lives, but a
|
||||
* manager belonging to a discarded runtime must release its thread, otherwise every
|
||||
* operator profile switch would leave another supervisor behind holding the dead
|
||||
* controller.</p>
|
||||
*/
|
||||
void shutdown() {
|
||||
scheduler.shutdownNow();
|
||||
LOGGER.fine("ON4KST connection supervisor shut down");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the current session and invalidates every scheduled callback or reconnect
|
||||
* belonging to it.
|
||||
|
||||
@@ -33,6 +33,24 @@ public final class SkedReminderService {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels every armed reminder and stops the scheduler thread.
|
||||
*
|
||||
* <p>Called when the runtime that owns this service is torn down, so a discarded
|
||||
* runtime does not keep a thread and pending reminders alive.</p>
|
||||
*/
|
||||
public void shutdown() {
|
||||
|
||||
for (List<ScheduledFuture<?>> remindersOfOneCall : scheduledByCallRaw.values()) {
|
||||
for (ScheduledFuture<?> armedReminder : remindersOfOneCall) {
|
||||
armedReminder.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
scheduledByCallRaw.clear();
|
||||
scheduler.shutdownNow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Arms reminders for one sked. Existing reminders for this call are cancelled.
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@ package kst4contest.view.map;
|
||||
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.util.Duration;
|
||||
@@ -54,6 +55,16 @@ public final class StationMapBridge {
|
||||
|
||||
private final PauseTransition refreshCoalescer = new PauseTransition(Duration.seconds(1.0));
|
||||
|
||||
/*
|
||||
* The listeners are kept so install() can be undone. Without that, the coalescing
|
||||
* animation and the registered listeners would keep a discarded runtime reachable
|
||||
* after an operator profile switch.
|
||||
*/
|
||||
private ListChangeListener<ChatMember> chatMemberListListener;
|
||||
private ChangeListener<ChatMember> selectedChatMemberListener;
|
||||
private ChangeListener<Number> antennaDirectionListener;
|
||||
private ListChangeListener<Predicate<ChatMember>> filterPredicateListener;
|
||||
|
||||
public StationMapBridge(ChatController chatController,
|
||||
TableView<ChatMember> chatMemberTable,
|
||||
StationMapView stationMapView,
|
||||
@@ -81,25 +92,59 @@ public final class StationMapBridge {
|
||||
|
||||
stationMapView.setOnResetView(this::handleMapReset);
|
||||
|
||||
chatController.getLst_chatMemberSortedFilteredList().addListener(
|
||||
(ListChangeListener<ChatMember>) change -> scheduleRefresh()
|
||||
);
|
||||
chatMemberListListener = change -> scheduleRefresh();
|
||||
chatController.getLst_chatMemberSortedFilteredList().addListener(chatMemberListListener);
|
||||
|
||||
chatController.getScoreService().selectedChatMemberProperty().addListener(
|
||||
(obs, oldValue, newValue) -> requestImmediateRefresh()
|
||||
);
|
||||
selectedChatMemberListener = (obs, oldValue, newValue) -> requestImmediateRefresh();
|
||||
chatController.getScoreService().selectedChatMemberProperty()
|
||||
.addListener(selectedChatMemberListener);
|
||||
|
||||
chatController.getChatPreferences().getActualQTF().addListener(
|
||||
(obs, oldValue, newValue) -> scheduleRefresh()
|
||||
);
|
||||
antennaDirectionListener = (obs, oldValue, newValue) -> scheduleRefresh();
|
||||
chatController.getChatPreferences().getActualQTF().addListener(antennaDirectionListener);
|
||||
|
||||
chatController.getLst_chatMemberListFilterPredicates().addListener(
|
||||
(ListChangeListener<Predicate<ChatMember>>) change -> requestImmediateRefresh()
|
||||
);
|
||||
filterPredicateListener = change -> requestImmediateRefresh();
|
||||
chatController.getLst_chatMemberListFilterPredicates().addListener(filterPredicateListener);
|
||||
|
||||
requestImmediateRefresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes everything {@link #install()} registered and stops the coalescing timer.
|
||||
*
|
||||
* <p>Needed when the runtime owning this bridge is discarded, for example during an
|
||||
* operator profile switch. A running {@link PauseTransition} would otherwise keep
|
||||
* firing into a dead user interface.</p>
|
||||
*/
|
||||
public void uninstall() {
|
||||
|
||||
refreshCoalescer.stop();
|
||||
|
||||
stationMapView.setOnCallsignRawSelected(null);
|
||||
stationMapView.setOnTriggerClusterSpot(null);
|
||||
stationMapView.setOnResetView(null);
|
||||
|
||||
if (chatMemberListListener != null) {
|
||||
chatController.getLst_chatMemberSortedFilteredList().removeListener(chatMemberListListener);
|
||||
chatMemberListListener = null;
|
||||
}
|
||||
|
||||
if (selectedChatMemberListener != null) {
|
||||
chatController.getScoreService().selectedChatMemberProperty()
|
||||
.removeListener(selectedChatMemberListener);
|
||||
selectedChatMemberListener = null;
|
||||
}
|
||||
|
||||
if (antennaDirectionListener != null) {
|
||||
chatController.getChatPreferences().getActualQTF().removeListener(antennaDirectionListener);
|
||||
antennaDirectionListener = null;
|
||||
}
|
||||
|
||||
if (filterPredicateListener != null) {
|
||||
chatController.getLst_chatMemberListFilterPredicates().removeListener(filterPredicateListener);
|
||||
filterPredicateListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMapReset() {
|
||||
Runnable resetAction = () -> {
|
||||
/*
|
||||
|
||||
@@ -244,6 +244,25 @@ public final class StationMapView {
|
||||
stage.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases every resource this map window owns.
|
||||
*
|
||||
* <p>The tile proxy is a local server socket with its own thread pool. It used to
|
||||
* live until the process ended, which was harmless while the map existed exactly
|
||||
* once per process. A runtime that is discarded, for example during an operator
|
||||
* profile switch, has to hand it back.</p>
|
||||
*/
|
||||
public void dispose() {
|
||||
|
||||
if (tileProxyServer != null) {
|
||||
tileProxyServer.stop();
|
||||
tileProxyServer = null;
|
||||
}
|
||||
|
||||
webEngine.load(null);
|
||||
stage.close();
|
||||
}
|
||||
|
||||
public boolean isShowing() {
|
||||
return stage.isShowing();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user