diff --git a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiModelCatalogCoordinator.java b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiModelCatalogCoordinator.java index d50ef1d..636c524 100644 --- a/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiModelCatalogCoordinator.java +++ b/pdf-umbenenner-adapter-in-gui/src/main/java/de/gecheckt/pdf/umbenenner/adapter/in/gui/GuiModelCatalogCoordinator.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import org.apache.logging.log4j.LogManager; @@ -35,6 +36,13 @@ import javafx.application.Platform; * completed retrieval attempt, so later GUI layers can display the result. * *

+ * Parallele Abrufanfragen (z. B. durch schnellen Provider-Wechsel oder mehrfaches Klicken + * auf „Modelle neu laden") werden durch einen Generationszähler entschärft: Jede neue Anfrage + * erhöht den Zähler. Wenn das Ergebnis eines Hintergrund-Threads auf dem JavaFX-Thread + * verarbeitet wird, prüft der Coordinator, ob die Generationsnummer noch aktuell ist. Veraltete + * Ergebnisse (aus einer früheren Anfrage) werden verworfen, sodass stets nur das Ergebnis der + * jüngsten Anfrage in die Meldungsliste und die Feldcontainer geschrieben wird. + *

* The worker thread factory is injectable so tests can supply a synchronous or latch-guarded * executor without spinning a real OS thread. *

@@ -62,6 +70,14 @@ public final class GuiModelCatalogCoordinator { private final Map fieldContainers = new ConcurrentHashMap<>(); + /** + * Generationszähler zur Erkennung veralteter Abruf-Ergebnisse. + * Wird bei jeder neuen Anfrage in {@link #triggerModelRetrieval} atomar erhöht. + * Hintergrund-Threads erfassen die Generation beim Start; auf dem JavaFX-Thread wird + * das Ergebnis verworfen, wenn die gespeicherte Generation nicht mehr aktuell ist. + */ + private final AtomicLong retrievalGeneration = new AtomicLong(0); + /** * Consumer that delivers the retrieval result. In production this wraps the call in * {@code Platform.runLater}. In tests it can be replaced with a direct call so the result @@ -144,12 +160,23 @@ public final class GuiModelCatalogCoordinator { // Build the request from the current editor state. ModelCatalogRequest request = buildRequest(family, providerState); - LOG.info("GUI-Modellabruf: Modelllistenabruf für Provider '{}' gestartet.", - family.getIdentifier()); + // Generationsnummer erhöhen – laufende Hintergrund-Threads mit einer älteren + // Generationsnummer verwerfen ihr Ergebnis, sobald sie auf dem FX-Thread ankommen. + long currentGeneration = retrievalGeneration.incrementAndGet(); + + LOG.info("GUI-Modellabruf: Modelllistenabruf für Provider '{}' gestartet (Generation {}).", + family.getIdentifier(), currentGeneration); Runnable task = () -> { ModelCatalogResult result = modelCatalogPort.fetchAvailableModels(request); resultDelivery.accept(() -> { + // Veraltetes Ergebnis verwerfen, wenn inzwischen eine neuere Anfrage gestartet wurde. + if (retrievalGeneration.get() != currentGeneration) { + LOG.debug("GUI-Modellabruf: Ergebnis für Provider '{}' verworfen" + + " (Generation {} ist nicht mehr aktuell).", + family.getIdentifier(), currentGeneration); + return; + } applyResult(family, container, result, previousManualValue); postResultCallback.run(); });