Fix #54: Modellabruf ueber Generation-Counter gegen veraltete Ergebnisse absichern

Bei mehrfachem Provider-Wechsel oder Modelle-Neu-Laden konnten parallele
HTTP-Threads ihre Ergebnisse in dieselbe Meldungsliste schreiben. Mit
einem AtomicLong-Generationszaehler wird vor jedem Lauf eine Generation
festgehalten; bei der UI-Auslieferung auf dem JavaFX Application Thread
wird verworfen, was nicht mehr zur aktuellen Generation gehoert. Damit
ueberschreiben veraltete Worker den UI-Zustand nicht mehr.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 06:21:15 +02:00
parent a87c73401b
commit d10a572b50
@@ -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.</li>
* </ul>
* <p>
* Parallele Abrufanfragen (z.&nbsp;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.
* <p>
* The worker thread factory is injectable so tests can supply a synchronous or latch-guarded
* executor without spinning a real OS thread.
* <p>
@@ -62,6 +70,14 @@ public final class GuiModelCatalogCoordinator {
private final Map<AiProviderFamily, GuiModelFieldContainer> 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();
});