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:
+58
@@ -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 ≥ 0 sein
|
||||
* @param additionalFailed hinzuzurechnende Fehler-Zahl; muss ≥ 0 sein
|
||||
* @return aufaddiertes Sitzungstotal; nie {@code null}
|
||||
*/
|
||||
public SchedulerSessionTotals plus(int additionalSuccess, int additionalFailed) {
|
||||
return new SchedulerSessionTotals(
|
||||
successCount + additionalSuccess,
|
||||
failedCount + additionalFailed);
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
+18
-4
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+88
@@ -19,6 +19,7 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
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;
|
||||
@@ -63,6 +64,93 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
assertThat(status.lastRunSummary()).isEmpty();
|
||||
assertThat(status.nextTickAt()).isEmpty();
|
||||
assertThat(status.lastError()).isEmpty();
|
||||
assertThat(status.sessionTotals()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_initializesSessionTotalsToZero() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
|
||||
useCase.start();
|
||||
|
||||
SchedulerStatus status = useCase.getStatus();
|
||||
assertThat(status.sessionTotals()).contains(SchedulerSessionTotals.zero());
|
||||
}
|
||||
|
||||
@Test
|
||||
void tick_started_accumulatesIntoSessionTotals() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
useCase.start();
|
||||
when(batchRunTrigger.triggerRun())
|
||||
.thenReturn(new BatchRunTriggerResult.Started(Instant.now(), new RunSummary(2, 1, 4)))
|
||||
.thenReturn(new BatchRunTriggerResult.Started(Instant.now(), new RunSummary(3, 0, 0)));
|
||||
|
||||
useCase.executeWrappedTick();
|
||||
useCase.executeWrappedTick();
|
||||
|
||||
SchedulerStatus status = useCase.getStatus();
|
||||
assertThat(status.sessionTotals()).contains(new SchedulerSessionTotals(5, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void tick_skippedBusy_doesNotChangeSessionTotals() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
useCase.start();
|
||||
when(batchRunTrigger.triggerRun())
|
||||
.thenReturn(new BatchRunTriggerResult.Started(Instant.now(), new RunSummary(2, 1, 0)))
|
||||
.thenReturn(new BatchRunTriggerResult.SkippedBusy());
|
||||
|
||||
useCase.executeWrappedTick();
|
||||
useCase.executeWrappedTick();
|
||||
|
||||
SchedulerStatus status = useCase.getStatus();
|
||||
assertThat(status.sessionTotals()).contains(new SchedulerSessionTotals(2, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void tick_failed_doesNotChangeSessionTotals() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
useCase.start();
|
||||
when(batchRunTrigger.triggerRun())
|
||||
.thenReturn(new BatchRunTriggerResult.Started(Instant.now(), new RunSummary(1, 1, 0)))
|
||||
.thenReturn(new BatchRunTriggerResult.Failed("Fehler", "tech"));
|
||||
|
||||
useCase.executeWrappedTick();
|
||||
useCase.executeWrappedTick();
|
||||
|
||||
SchedulerStatus status = useCase.getStatus();
|
||||
assertThat(status.sessionTotals()).contains(new SchedulerSessionTotals(1, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stop_keepsSessionTotalsVisible() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
useCase.start();
|
||||
when(batchRunTrigger.triggerRun())
|
||||
.thenReturn(new BatchRunTriggerResult.Started(Instant.now(), new RunSummary(7, 2, 0)));
|
||||
useCase.executeWrappedTick();
|
||||
|
||||
useCase.stop();
|
||||
|
||||
SchedulerStatus status = useCase.getStatus();
|
||||
assertThat(status.state()).isEqualTo(SchedulerState.STOPPED);
|
||||
assertThat(status.sessionTotals()).contains(new SchedulerSessionTotals(7, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_afterPreviousSession_resetsSessionTotalsToZero() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
useCase.start();
|
||||
when(batchRunTrigger.triggerRun())
|
||||
.thenReturn(new BatchRunTriggerResult.Started(Instant.now(), new RunSummary(5, 3, 0)));
|
||||
useCase.executeWrappedTick();
|
||||
useCase.stop();
|
||||
assertThat(useCase.getStatus().sessionTotals())
|
||||
.contains(new SchedulerSessionTotals(5, 3));
|
||||
|
||||
useCase.start();
|
||||
|
||||
assertThat(useCase.getStatus().sessionTotals()).contains(SchedulerSessionTotals.zero());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user