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:
+29
-2
@@ -5,6 +5,7 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
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>
|
* completed retrieval attempt, so later GUI layers can display the result.</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
* <p>
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
* The worker thread factory is injectable so tests can supply a synchronous or latch-guarded
|
* The worker thread factory is injectable so tests can supply a synchronous or latch-guarded
|
||||||
* executor without spinning a real OS thread.
|
* executor without spinning a real OS thread.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -62,6 +70,14 @@ public final class GuiModelCatalogCoordinator {
|
|||||||
private final Map<AiProviderFamily, GuiModelFieldContainer> fieldContainers =
|
private final Map<AiProviderFamily, GuiModelFieldContainer> fieldContainers =
|
||||||
new ConcurrentHashMap<>();
|
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
|
* 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
|
* {@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.
|
// Build the request from the current editor state.
|
||||||
ModelCatalogRequest request = buildRequest(family, providerState);
|
ModelCatalogRequest request = buildRequest(family, providerState);
|
||||||
|
|
||||||
LOG.info("GUI-Modellabruf: Modelllistenabruf für Provider '{}' gestartet.",
|
// Generationsnummer erhöhen – laufende Hintergrund-Threads mit einer älteren
|
||||||
family.getIdentifier());
|
// 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 = () -> {
|
Runnable task = () -> {
|
||||||
ModelCatalogResult result = modelCatalogPort.fetchAvailableModels(request);
|
ModelCatalogResult result = modelCatalogPort.fetchAvailableModels(request);
|
||||||
resultDelivery.accept(() -> {
|
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);
|
applyResult(family, container, result, previousManualValue);
|
||||||
postResultCallback.run();
|
postResultCallback.run();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user