Feature: Scheduler-Tick-Zaehlung korrigieren und Sitzungstotale einfuehren

Der Scheduler-Tab meldete nach erfolgreicher Verarbeitung faelschlich
"keine neuen Dokumente". Ursache war ein hartkodiertes RunSummary.noOp()
im BatchRunTrigger der Bootstrap; der echte Lauf-Summary wurde nie
gelesen.

- Bootstrap: BatchRunProgressObserver erfasst RunSummary aus onRunEnded
  und uebersetzt ihn in den ausgehenden RunSummary fuer das Tick-Ergebnis
- Neuer Wert-Typ SchedulerSessionTotals (success/failed) plus
  Optional-Feld in SchedulerStatus
- DefaultSchedulerControlUseCase setzt die Totale beim start() auf null
  zurueck, summiert pro Started-Tick auf, friert sie beim stop() ein
- GuiSchedulerTab zeigt pro Tick "X verarbeitet, Y Fehler" oder
  "keine neuen Dokumente" sowie ein zusaetzliches Label
  "Seit Scheduler-Start: X verarbeitet, Y Fehler"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 14:51:36 +02:00
parent ac5b74917f
commit 368cb81b56
6 changed files with 233 additions and 10 deletions
@@ -0,0 +1,58 @@
package de.gecheckt.pdf.umbenenner.application.port.in;
/**
* Aggregierte Zähler über alle abgeschlossenen Ticks der laufenden bzw. zuletzt
* gelaufenen Scheduler-Sitzung.
* <p>
* Eine Sitzung beginnt mit dem nächsten erfolgreichen
* {@link SchedulerControlUseCase#start()} und endet mit dem zugehörigen
* {@link SchedulerControlUseCase#stop()}. Beim Start einer neuen Sitzung
* werden die Zähler auf null zurückgesetzt; nach dem Stopp bleiben sie
* eingefroren sichtbar, bis der Scheduler erneut gestartet wird.
* <p>
* Übersprungene Dokumente werden in dieser Sitzungsstatistik bewusst nicht
* gezählt, da sie für den Bediener keine neue Verarbeitungsleistung darstellen.
*
* @param successCount Summe aller erfolgreich verarbeiteten Dokumente seit
* Sitzungsstart; nie negativ
* @param failedCount Summe aller fehlgeschlagenen Dokumente seit Sitzungsstart
* (retryable und final zusammengefasst); nie negativ
*/
public record SchedulerSessionTotals(int successCount, int failedCount) {
/**
* Validiert, dass beide Zähler nicht negativ sind.
*
* @throws IllegalArgumentException wenn einer der Zähler kleiner als null ist
*/
public SchedulerSessionTotals {
if (successCount < 0 || failedCount < 0) {
throw new IllegalArgumentException(
"SchedulerSessionTotals counts must not be negative; was: "
+ successCount + "/" + failedCount);
}
}
/**
* Liefert ein neutrales Ausgangsobjekt mit beiden Zählern auf null.
*
* @return Sitzungstotal mit allen Zählern auf null; nie {@code null}
*/
public static SchedulerSessionTotals zero() {
return new SchedulerSessionTotals(0, 0);
}
/**
* Liefert ein neues Sitzungstotal, in dem die übergebenen Werte additiv
* aufgenommen wurden. Das aktuelle Objekt bleibt unverändert.
*
* @param additionalSuccess hinzuzurechnende Erfolgreich-Zahl; muss &ge; 0 sein
* @param additionalFailed hinzuzurechnende Fehler-Zahl; muss &ge; 0 sein
* @return aufaddiertes Sitzungstotal; nie {@code null}
*/
public SchedulerSessionTotals plus(int additionalSuccess, int additionalFailed) {
return new SchedulerSessionTotals(
successCount + additionalSuccess,
failedCount + additionalFailed);
}
}
@@ -26,13 +26,19 @@ import de.gecheckt.pdf.umbenenner.application.port.out.RunSummary;
* @param lastError letzte aufgetretene deutsche Fehlermeldung;
* wird bei erfolgreichem Lauf gelöscht,
* bei {@code SkippedBusy} unverändert gelassen
* @param sessionTotals aggregierte Zähler seit dem letzten Sitzungsstart;
* leer vor dem allerersten {@code start()}, ab dem
* ersten erfolgreichen Start gefüllt und bei jedem
* weiteren Start auf null zurückgesetzt; nach dem
* Stopp bleibt der eingefrorene Endwert sichtbar
*/
public record SchedulerStatus(
SchedulerState state,
Optional<Instant> lastRunEndedAt,
Optional<RunSummary> lastRunSummary,
Optional<Instant> nextTickAt,
Optional<String> lastError
Optional<String> lastError,
Optional<SchedulerSessionTotals> sessionTotals
) {
/**
@@ -54,6 +60,9 @@ public record SchedulerStatus(
if (lastError == null) {
throw new IllegalArgumentException("lastError darf nicht null sein");
}
if (sessionTotals == null) {
throw new IllegalArgumentException("sessionTotals darf nicht null sein");
}
}
/**
@@ -70,6 +79,7 @@ public record SchedulerStatus(
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty(),
Optional.empty()
);
}
@@ -1,6 +1,7 @@
package de.gecheckt.pdf.umbenenner.application.usecase;
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerControlUseCase;
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerSessionTotals;
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerStartException;
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerState;
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerStatus;
@@ -132,9 +133,15 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
"Scheduler-Adapter konnte nicht gestartet werden.", e);
}
// Schritt 4: Zustand auf RUNNING_IDLE setzen
// Schritt 4: Zustand auf RUNNING_IDLE setzen, Sitzungstotal auf null zurücksetzen
Instant nextTick = Instant.now().plusSeconds(intervalSeconds);
statusRef.updateAndGet(s -> withState(s, SchedulerState.RUNNING_IDLE, Optional.of(nextTick)));
statusRef.updateAndGet(s -> new SchedulerStatus(
SchedulerState.RUNNING_IDLE,
s.lastRunEndedAt(),
s.lastRunSummary(),
Optional.of(nextTick),
s.lastError(),
Optional.of(SchedulerSessionTotals.zero())));
logger.info("Scheduler gestartet. Intervall: {} Sekunden.", intervalSeconds);
}
@@ -247,12 +254,17 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
Optional<Instant> lastRunEndedAt = afterBatch.lastRunEndedAt();
Optional<RunSummary> lastRunSummary = afterBatch.lastRunSummary();
Optional<String> lastError = afterBatch.lastError();
Optional<SchedulerSessionTotals> sessionTotals = afterBatch.sessionTotals();
switch (result) {
case BatchRunTriggerResult.Started started -> {
lastRunEndedAt = Optional.of(started.endedAt());
lastRunSummary = Optional.of(started.summary());
lastError = Optional.empty();
sessionTotals = Optional.of(sessionTotals
.orElse(SchedulerSessionTotals.zero())
.plus(started.summary().successCount(),
started.summary().failedCount()));
logger.info("Scheduler-Tick abgeschlossen: {} erfolgreich, {} fehlgeschlagen, {} übersprungen.",
started.summary().successCount(),
started.summary().failedCount(),
@@ -271,7 +283,8 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
lastRunEndedAt,
lastRunSummary,
nextTickAt,
lastError));
lastError,
sessionTotals));
if (stopping) {
lockPort.releaseLock();
@@ -292,6 +305,7 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
base.lastRunEndedAt(),
base.lastRunSummary(),
nextTickAt,
base.lastError());
base.lastError(),
base.sessionTotals());
}
}