mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-09-11 19:55:40 +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.stop();
|
||||||
rotatorClient = null;
|
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) {
|
private void cancelTimer(Timer timer) {
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
|
|||||||
@@ -139,6 +139,20 @@ final class On4KstConnectionManager {
|
|||||||
scheduler.execute(() -> openConnection(token));
|
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
|
* Stops the current session and invalidates every scheduled callback or reconnect
|
||||||
* belonging to it.
|
* belonging to it.
|
||||||
|
|||||||
@@ -33,6 +33,24 @@ public final class SkedReminderService {
|
|||||||
this.controller = controller;
|
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.
|
* 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.animation.PauseTransition;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.scene.control.TableView;
|
import javafx.scene.control.TableView;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
@@ -54,6 +55,16 @@ public final class StationMapBridge {
|
|||||||
|
|
||||||
private final PauseTransition refreshCoalescer = new PauseTransition(Duration.seconds(1.0));
|
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,
|
public StationMapBridge(ChatController chatController,
|
||||||
TableView<ChatMember> chatMemberTable,
|
TableView<ChatMember> chatMemberTable,
|
||||||
StationMapView stationMapView,
|
StationMapView stationMapView,
|
||||||
@@ -81,25 +92,59 @@ public final class StationMapBridge {
|
|||||||
|
|
||||||
stationMapView.setOnResetView(this::handleMapReset);
|
stationMapView.setOnResetView(this::handleMapReset);
|
||||||
|
|
||||||
chatController.getLst_chatMemberSortedFilteredList().addListener(
|
chatMemberListListener = change -> scheduleRefresh();
|
||||||
(ListChangeListener<ChatMember>) change -> scheduleRefresh()
|
chatController.getLst_chatMemberSortedFilteredList().addListener(chatMemberListListener);
|
||||||
);
|
|
||||||
|
|
||||||
chatController.getScoreService().selectedChatMemberProperty().addListener(
|
selectedChatMemberListener = (obs, oldValue, newValue) -> requestImmediateRefresh();
|
||||||
(obs, oldValue, newValue) -> requestImmediateRefresh()
|
chatController.getScoreService().selectedChatMemberProperty()
|
||||||
);
|
.addListener(selectedChatMemberListener);
|
||||||
|
|
||||||
chatController.getChatPreferences().getActualQTF().addListener(
|
antennaDirectionListener = (obs, oldValue, newValue) -> scheduleRefresh();
|
||||||
(obs, oldValue, newValue) -> scheduleRefresh()
|
chatController.getChatPreferences().getActualQTF().addListener(antennaDirectionListener);
|
||||||
);
|
|
||||||
|
|
||||||
chatController.getLst_chatMemberListFilterPredicates().addListener(
|
filterPredicateListener = change -> requestImmediateRefresh();
|
||||||
(ListChangeListener<Predicate<ChatMember>>) change -> requestImmediateRefresh()
|
chatController.getLst_chatMemberListFilterPredicates().addListener(filterPredicateListener);
|
||||||
);
|
|
||||||
|
|
||||||
requestImmediateRefresh();
|
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() {
|
private void handleMapReset() {
|
||||||
Runnable resetAction = () -> {
|
Runnable resetAction = () -> {
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -244,6 +244,25 @@ public final class StationMapView {
|
|||||||
stage.hide();
|
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() {
|
public boolean isShowing() {
|
||||||
return stage.isShowing();
|
return stage.isShowing();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user