Scheduler: Autostart-Feature entfernen
Der Scheduler startet niemals automatisch beim Programmstart. Der Nutzer startet ihn ausschliesslich bewusst ueber den Start-Button im Scheduler-Tab. scheduler.enabled wird nicht mehr gelesen oder geschrieben; das Property ist obsolet. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+1
-71
@@ -40,9 +40,6 @@ import javafx.scene.layout.VBox;
|
||||
*
|
||||
* <h2>Bereiche</h2>
|
||||
* <ul>
|
||||
* <li><strong>Autostart-Fehler-Banner</strong>: Sichtbar wenn beim Programmstart
|
||||
* ein konfigurierter Autostart fehlgeschlagen ist. Bietet Schnellaktionen zum
|
||||
* Starten des Schedulers oder zum Deaktivieren des Autostarts.</li>
|
||||
* <li><strong>Scheduler-Steuerung</strong>: Status-Anzeige (● Aktiv / ○ Gestoppt),
|
||||
* Start-/Stopp-Schaltflächen, Countdown bis zum nächsten Lauf,
|
||||
* Letzter-Lauf-Info, Fehlermeldung und Intervall-Konfiguration.</li>
|
||||
@@ -88,21 +85,6 @@ public final class GuiSchedulerTab {
|
||||
private final TextField intervalField = new TextField();
|
||||
private final Label intervalValidationLabel = new Label();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Bereich 2: Autostart-Fehler-Banner
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private final VBox autostartErrorBanner = new VBox(6);
|
||||
private final Label autostartErrorDetailLabel = new Label();
|
||||
private final Button autostartStartButton = new Button("Scheduler starten");
|
||||
private final Button autostartDisableButton = new Button("Autostart deaktivieren");
|
||||
|
||||
/**
|
||||
* Wenn {@code true}, wird das Autostart-Fehler-Banner dauerhaft ausgeblendet
|
||||
* (weil der Benutzer „Autostart deaktivieren" geklickt hat).
|
||||
*/
|
||||
private boolean autostartBannerDismissed = false;
|
||||
|
||||
private final ExecutorService workerExecutor = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "gui-scheduler-control");
|
||||
t.setDaemon(true);
|
||||
@@ -180,7 +162,6 @@ public final class GuiSchedulerTab {
|
||||
updateLastRunLabel(status);
|
||||
updateLastErrorLabel(status);
|
||||
updateIntervalFieldEditability(status);
|
||||
updateAutostartBanner(status);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -188,27 +169,11 @@ public final class GuiSchedulerTab {
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void buildUi() {
|
||||
buildAutostartBanner();
|
||||
VBox controlArea = buildControlArea();
|
||||
VBox content = new VBox(0, autostartErrorBanner, controlArea);
|
||||
tab.setContent(content);
|
||||
tab.setContent(controlArea);
|
||||
wireActions();
|
||||
}
|
||||
|
||||
private void buildAutostartBanner() {
|
||||
Label autostartTitleLabel = new Label("⚠ Autostart fehlgeschlagen – Scheduler ist nicht aktiv.");
|
||||
autostartTitleLabel.setStyle("-fx-font-weight: bold;");
|
||||
autostartErrorDetailLabel.setWrapText(true);
|
||||
HBox bannerButtons = new HBox(10, autostartStartButton, autostartDisableButton);
|
||||
autostartErrorBanner.getChildren().addAll(
|
||||
autostartTitleLabel, autostartErrorDetailLabel, bannerButtons);
|
||||
autostartErrorBanner.setStyle(
|
||||
"-fx-background-color: #fff3cd; -fx-border-color: #ffc107;"
|
||||
+ " -fx-border-width: 1; -fx-padding: 10;");
|
||||
autostartErrorBanner.setVisible(false);
|
||||
autostartErrorBanner.setManaged(false);
|
||||
}
|
||||
|
||||
private VBox buildControlArea() {
|
||||
statusLabel.setStyle(HEADER_LABEL_STYLE);
|
||||
|
||||
@@ -251,8 +216,6 @@ public final class GuiSchedulerTab {
|
||||
private void wireActions() {
|
||||
startButton.setOnAction(e -> executeStart());
|
||||
stopButton.setOnAction(e -> executeStop());
|
||||
autostartStartButton.setOnAction(e -> executeStart());
|
||||
autostartDisableButton.setOnAction(e -> executeDisableAutostart());
|
||||
|
||||
intervalField.focusedProperty().addListener((obs, wasFocused, focused) -> {
|
||||
if (!focused) {
|
||||
@@ -334,7 +297,6 @@ public final class GuiSchedulerTab {
|
||||
stopButton.setDisable(true);
|
||||
}
|
||||
}
|
||||
autostartStartButton.setDisable(startButton.isDisable());
|
||||
}
|
||||
|
||||
private void updateNextTickLabel(SchedulerStatus status) {
|
||||
@@ -394,17 +356,6 @@ public final class GuiSchedulerTab {
|
||||
intervalField.setDisable(!editable);
|
||||
}
|
||||
|
||||
private void updateAutostartBanner(SchedulerStatus status) {
|
||||
boolean show = status.autostartFailed()
|
||||
&& !autostartBannerDismissed
|
||||
&& !status.state().isActive();
|
||||
autostartErrorBanner.setVisible(show);
|
||||
autostartErrorBanner.setManaged(show);
|
||||
if (show) {
|
||||
status.lastError().ifPresent(autostartErrorDetailLabel::setText);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Aktions-Handler
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -412,7 +363,6 @@ public final class GuiSchedulerTab {
|
||||
private void executeStart() {
|
||||
LOG.info("GUI: Scheduler-Start angefordert.");
|
||||
startButton.setDisable(true);
|
||||
autostartStartButton.setDisable(true);
|
||||
stopButton.setDisable(true);
|
||||
workerExecutor.submit(() -> schedulerUseCase.ifPresent(uc -> {
|
||||
try {
|
||||
@@ -442,26 +392,6 @@ public final class GuiSchedulerTab {
|
||||
}));
|
||||
}
|
||||
|
||||
private void executeDisableAutostart() {
|
||||
LOG.info("GUI: Autostart-Deaktivierung angefordert.");
|
||||
autostartDisableButton.setDisable(true);
|
||||
workerExecutor.submit(() -> schedulerUseCase.ifPresent(uc -> {
|
||||
try {
|
||||
uc.disableAutostart();
|
||||
LOG.info("GUI: Autostart erfolgreich deaktiviert.");
|
||||
Platform.runLater(() -> {
|
||||
autostartBannerDismissed = true;
|
||||
autostartErrorBanner.setVisible(false);
|
||||
autostartErrorBanner.setManaged(false);
|
||||
autostartDisableButton.setDisable(false);
|
||||
});
|
||||
} catch (RuntimeException e) {
|
||||
LOG.error("GUI: Fehler beim Deaktivieren des Autostarts.", e);
|
||||
Platform.runLater(() -> autostartDisableButton.setDisable(false));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void validateAndSaveInterval() {
|
||||
String text = intervalField.getText() == null ? "" : intervalField.getText().trim();
|
||||
try {
|
||||
|
||||
+1
-24
@@ -56,7 +56,6 @@ public class FileChannelConfigurationAccessAdapter
|
||||
private static final long ACQUIRE_TIMEOUT_MS = 3000L;
|
||||
private static final long ACQUIRE_RETRY_INTERVAL_MS = 100L;
|
||||
|
||||
private static final String KEY_ENABLED = "scheduler.enabled";
|
||||
private static final String KEY_INTERVAL = "scheduler.interval.seconds";
|
||||
|
||||
private final Path configFile;
|
||||
@@ -191,23 +190,8 @@ public class FileChannelConfigurationAccessAdapter
|
||||
+ "Standardwerte werden verwendet. Datei: {}", configFile, e);
|
||||
return SchedulerSettings.defaults();
|
||||
}
|
||||
boolean enabled = parseEnabled(props.getProperty(KEY_ENABLED));
|
||||
int intervalSeconds = parseInterval(props.getProperty(KEY_INTERVAL));
|
||||
return new SchedulerSettings(enabled, intervalSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt den Wert von {@code scheduler.enabled} in die Konfigurationsdatei.
|
||||
* <p>
|
||||
* Alle übrigen Inhalte der Datei bleiben unverändert. Existiert der Key
|
||||
* noch nicht, wird er am Ende der Datei ergänzt.
|
||||
*
|
||||
* @param enabled neuer Wert für {@code scheduler.enabled}
|
||||
* @throws SchedulerSettingsWriteException wenn der Schreibvorgang fehlschlägt
|
||||
*/
|
||||
@Override
|
||||
public void saveEnabled(boolean enabled) throws SchedulerSettingsWriteException {
|
||||
updateProperty(KEY_ENABLED, String.valueOf(enabled));
|
||||
return new SchedulerSettings(intervalSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,13 +213,6 @@ public class FileChannelConfigurationAccessAdapter
|
||||
// Hilfsmethoden: Parsen
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private boolean parseEnabled(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return SchedulerSettings.DEFAULT_ENABLED;
|
||||
}
|
||||
return Boolean.parseBoolean(raw.trim());
|
||||
}
|
||||
|
||||
private int parseInterval(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return SchedulerSettings.DEFAULT_INTERVAL_SECONDS;
|
||||
|
||||
+45
-130
@@ -16,22 +16,12 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationFileLockException;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerSettings;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerSettingsWriteException;
|
||||
|
||||
/**
|
||||
* Unit-Tests für {@link FileChannelConfigurationAccessAdapter}.
|
||||
* <p>
|
||||
* Deckt das Lock-Protokoll (Erwerb, Freigabe, Idempotenz), die Settings-Lese-
|
||||
* und Schreiblogik sowie die format-erhaltenden Zeileneigenschaften ab.
|
||||
* Alle Tests arbeiten auf einer temporären {@code .properties}-Datei im
|
||||
* JUnit-eigenen {@code @TempDir}.
|
||||
*/
|
||||
class FileChannelConfigurationAccessAdapterTest {
|
||||
|
||||
// =========================================================================
|
||||
// Lock-Protokoll
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void isLocked_returnsFalseBeforeAnyAcquire(@TempDir Path tempDir) throws IOException {
|
||||
Path config = createConfigFile(tempDir, "");
|
||||
@@ -115,10 +105,6 @@ class FileChannelConfigurationAccessAdapterTest {
|
||||
.isInstanceOf(ConfigurationFileLockException.class);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// loadSettings
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void loadSettings_returnsDefaultsWhenKeysAreMissing(@TempDir Path tempDir) throws IOException {
|
||||
Path config = createConfigFile(tempDir, "source.folder=S:\\source\n");
|
||||
@@ -127,27 +113,25 @@ class FileChannelConfigurationAccessAdapterTest {
|
||||
|
||||
SchedulerSettings settings = adapter.loadSettings();
|
||||
|
||||
assertThat(settings.enabled()).isEqualTo(SchedulerSettings.DEFAULT_ENABLED);
|
||||
assertThat(settings.intervalSeconds()).isEqualTo(SchedulerSettings.DEFAULT_INTERVAL_SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadSettings_returnsConfiguredValues(@TempDir Path tempDir) throws IOException {
|
||||
String content = "scheduler.enabled=true\nscheduler.interval.seconds=300\n";
|
||||
String content = "scheduler.interval.seconds=300\n";
|
||||
Path config = createConfigFile(tempDir, content);
|
||||
FileChannelConfigurationAccessAdapter adapter =
|
||||
new FileChannelConfigurationAccessAdapter(config);
|
||||
|
||||
SchedulerSettings settings = adapter.loadSettings();
|
||||
|
||||
assertThat(settings.enabled()).isTrue();
|
||||
assertThat(settings.intervalSeconds()).isEqualTo(300);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadSettings_returnsDefaultIntervalForNonNumericValue(@TempDir Path tempDir)
|
||||
throws IOException {
|
||||
String content = "scheduler.enabled=false\nscheduler.interval.seconds=not-a-number\n";
|
||||
String content = "scheduler.interval.seconds=not-a-number\n";
|
||||
Path config = createConfigFile(tempDir, content);
|
||||
FileChannelConfigurationAccessAdapter adapter =
|
||||
new FileChannelConfigurationAccessAdapter(config);
|
||||
@@ -157,19 +141,6 @@ class FileChannelConfigurationAccessAdapterTest {
|
||||
assertThat(settings.intervalSeconds()).isEqualTo(SchedulerSettings.DEFAULT_INTERVAL_SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadSettings_returnsDefaultEnabledForBlankValue(@TempDir Path tempDir)
|
||||
throws IOException {
|
||||
String content = "scheduler.enabled=\nscheduler.interval.seconds=180\n";
|
||||
Path config = createConfigFile(tempDir, content);
|
||||
FileChannelConfigurationAccessAdapter adapter =
|
||||
new FileChannelConfigurationAccessAdapter(config);
|
||||
|
||||
SchedulerSettings settings = adapter.loadSettings();
|
||||
|
||||
assertThat(settings.enabled()).isEqualTo(SchedulerSettings.DEFAULT_ENABLED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadSettings_returnsDefaultsWhenFileIsEmpty(@TempDir Path tempDir) throws IOException {
|
||||
Path config = createConfigFile(tempDir, "");
|
||||
@@ -181,78 +152,10 @@ class FileChannelConfigurationAccessAdapterTest {
|
||||
assertThat(settings).isEqualTo(SchedulerSettings.defaults());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// saveEnabled
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void saveEnabled_updatesExistingKeyAndPreservesOtherLines(@TempDir Path tempDir)
|
||||
throws IOException {
|
||||
String initial = "source.folder=/opt/source\nscheduler.enabled=false\ntarget.folder=/opt/target\n";
|
||||
Path config = createConfigFile(tempDir, initial);
|
||||
FileChannelConfigurationAccessAdapter adapter =
|
||||
new FileChannelConfigurationAccessAdapter(config);
|
||||
|
||||
adapter.saveEnabled(true);
|
||||
|
||||
Properties props = loadProperties(config);
|
||||
assertThat(props.getProperty("scheduler.enabled")).isEqualTo("true");
|
||||
assertThat(props.getProperty("source.folder")).isEqualTo("/opt/source");
|
||||
assertThat(props.getProperty("target.folder")).isEqualTo("/opt/target");
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveEnabled_appendsKeyWhenMissing(@TempDir Path tempDir) throws IOException {
|
||||
String initial = "source.folder=/opt/source\n";
|
||||
Path config = createConfigFile(tempDir, initial);
|
||||
FileChannelConfigurationAccessAdapter adapter =
|
||||
new FileChannelConfigurationAccessAdapter(config);
|
||||
|
||||
adapter.saveEnabled(true);
|
||||
|
||||
Properties props = loadProperties(config);
|
||||
assertThat(props.getProperty("scheduler.enabled")).isEqualTo("true");
|
||||
assertThat(props.getProperty("source.folder")).isEqualTo("/opt/source");
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveEnabled_writesCorrectlyThroughChannelWhenLocked(@TempDir Path tempDir)
|
||||
throws IOException {
|
||||
String initial = "scheduler.enabled=false\n";
|
||||
Path config = createConfigFile(tempDir, initial);
|
||||
FileChannelConfigurationAccessAdapter adapter =
|
||||
new FileChannelConfigurationAccessAdapter(config);
|
||||
|
||||
adapter.acquireLock();
|
||||
try {
|
||||
adapter.saveEnabled(true);
|
||||
} finally {
|
||||
adapter.releaseLock();
|
||||
}
|
||||
|
||||
Properties props = loadProperties(config);
|
||||
assertThat(props.getProperty("scheduler.enabled")).isEqualTo("true");
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveEnabled_throwsSchedulerSettingsWriteException_whenFileDoesNotExist(
|
||||
@TempDir Path tempDir) {
|
||||
Path nonExistent = tempDir.resolve("missing.properties");
|
||||
FileChannelConfigurationAccessAdapter adapter =
|
||||
new FileChannelConfigurationAccessAdapter(nonExistent);
|
||||
|
||||
assertThatThrownBy(() -> adapter.saveEnabled(true))
|
||||
.isInstanceOf(SchedulerSettingsWriteException.class);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// saveIntervalSeconds
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void saveIntervalSeconds_updatesExistingKeyAndPreservesOtherLines(@TempDir Path tempDir)
|
||||
throws IOException {
|
||||
String initial = "scheduler.interval.seconds=180\nscheduler.enabled=false\n";
|
||||
String initial = "source.folder=/opt/source\nscheduler.interval.seconds=180\ntarget.folder=/opt/target\n";
|
||||
Path config = createConfigFile(tempDir, initial);
|
||||
FileChannelConfigurationAccessAdapter adapter =
|
||||
new FileChannelConfigurationAccessAdapter(config);
|
||||
@@ -261,12 +164,13 @@ class FileChannelConfigurationAccessAdapterTest {
|
||||
|
||||
Properties props = loadProperties(config);
|
||||
assertThat(props.getProperty("scheduler.interval.seconds")).isEqualTo("300");
|
||||
assertThat(props.getProperty("scheduler.enabled")).isEqualTo("false");
|
||||
assertThat(props.getProperty("source.folder")).isEqualTo("/opt/source");
|
||||
assertThat(props.getProperty("target.folder")).isEqualTo("/opt/target");
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveIntervalSeconds_appendsKeyWhenMissing(@TempDir Path tempDir) throws IOException {
|
||||
String initial = "scheduler.enabled=true\n";
|
||||
String initial = "source.folder=/opt/source\n";
|
||||
Path config = createConfigFile(tempDir, initial);
|
||||
FileChannelConfigurationAccessAdapter adapter =
|
||||
new FileChannelConfigurationAccessAdapter(config);
|
||||
@@ -275,47 +179,58 @@ class FileChannelConfigurationAccessAdapterTest {
|
||||
|
||||
Properties props = loadProperties(config);
|
||||
assertThat(props.getProperty("scheduler.interval.seconds")).isEqualTo("240");
|
||||
assertThat(props.getProperty("scheduler.enabled")).isEqualTo("true");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Zeilenenden-Erhaltung
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void saveEnabled_preservesCrlfLineEndings(@TempDir Path tempDir) throws IOException {
|
||||
String initial = "scheduler.enabled=false\r\nother.key=value\r\n";
|
||||
Path config = createConfigFileBinary(tempDir, initial.getBytes(StandardCharsets.UTF_8));
|
||||
FileChannelConfigurationAccessAdapter adapter =
|
||||
new FileChannelConfigurationAccessAdapter(config);
|
||||
|
||||
adapter.saveEnabled(true);
|
||||
|
||||
byte[] resultBytes = Files.readAllBytes(config);
|
||||
String result = new String(resultBytes, StandardCharsets.UTF_8);
|
||||
assertThat(result).contains("scheduler.enabled=true\r\n");
|
||||
assertThat(result).contains("other.key=value\r\n");
|
||||
assertThat(props.getProperty("source.folder")).isEqualTo("/opt/source");
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveEnabled_preservesLfLineEndings(@TempDir Path tempDir) throws IOException {
|
||||
String initial = "scheduler.enabled=false\nother.key=value\n";
|
||||
void saveIntervalSeconds_writesCorrectlyThroughChannelWhenLocked(@TempDir Path tempDir)
|
||||
throws IOException {
|
||||
String initial = "scheduler.interval.seconds=180\n";
|
||||
Path config = createConfigFile(tempDir, initial);
|
||||
FileChannelConfigurationAccessAdapter adapter =
|
||||
new FileChannelConfigurationAccessAdapter(config);
|
||||
|
||||
adapter.saveEnabled(true);
|
||||
adapter.acquireLock();
|
||||
try {
|
||||
adapter.saveIntervalSeconds(300);
|
||||
} finally {
|
||||
adapter.releaseLock();
|
||||
}
|
||||
|
||||
Properties props = loadProperties(config);
|
||||
assertThat(props.getProperty("scheduler.interval.seconds")).isEqualTo("300");
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveIntervalSeconds_preservesCrlfLineEndings(@TempDir Path tempDir) throws IOException {
|
||||
String initial = "scheduler.interval.seconds=180\r\nother.key=value\r\n";
|
||||
Path config = createConfigFileBinary(tempDir, initial.getBytes(StandardCharsets.UTF_8));
|
||||
FileChannelConfigurationAccessAdapter adapter =
|
||||
new FileChannelConfigurationAccessAdapter(config);
|
||||
|
||||
adapter.saveIntervalSeconds(300);
|
||||
|
||||
byte[] resultBytes = Files.readAllBytes(config);
|
||||
String result = new String(resultBytes, StandardCharsets.UTF_8);
|
||||
assertThat(result).contains("scheduler.interval.seconds=300\r\n");
|
||||
assertThat(result).contains("other.key=value\r\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveIntervalSeconds_preservesLfLineEndings(@TempDir Path tempDir) throws IOException {
|
||||
String initial = "scheduler.interval.seconds=180\nother.key=value\n";
|
||||
Path config = createConfigFile(tempDir, initial);
|
||||
FileChannelConfigurationAccessAdapter adapter =
|
||||
new FileChannelConfigurationAccessAdapter(config);
|
||||
|
||||
adapter.saveIntervalSeconds(300);
|
||||
|
||||
String result = Files.readString(config, StandardCharsets.UTF_8);
|
||||
assertThat(result).contains("scheduler.enabled=true\n");
|
||||
assertThat(result).contains("scheduler.interval.seconds=300\n");
|
||||
assertThat(result).contains("other.key=value\n");
|
||||
assertThat(result).doesNotContain("\r\n");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Hilfsmethoden
|
||||
// =========================================================================
|
||||
|
||||
private static Path createConfigFile(Path tempDir, String content) throws IOException {
|
||||
Path config = tempDir.resolve("test.properties");
|
||||
Files.writeString(config, content, StandardCharsets.UTF_8);
|
||||
|
||||
+1
-14
@@ -25,7 +25,6 @@ public interface SchedulerControlUseCase {
|
||||
* folgende Sequenz gestartet:
|
||||
* <ol>
|
||||
* <li>Zustand auf {@code STARTING} setzen</li>
|
||||
* <li>{@code scheduler.enabled=true} persistieren</li>
|
||||
* <li>Exklusiven OS-Lock auf Konfigurationsdatei erwerben</li>
|
||||
* <li>Scheduler-Adapter starten (erster Tick sofort)</li>
|
||||
* <li>Zustand auf {@code RUNNING_IDLE} setzen</li>
|
||||
@@ -45,8 +44,7 @@ public interface SchedulerControlUseCase {
|
||||
* Ist der Scheduler bereits gestoppt, hat dieser Aufruf keine Wirkung.
|
||||
* Läuft gerade ein Tick, wechselt der Zustand zu
|
||||
* {@code STOPPING_BATCH_ACTIVE}; der laufende Batch wird regulär
|
||||
* zu Ende geführt. Danach werden {@code scheduler.enabled=false}
|
||||
* persistiert und der OS-Lock freigegeben.
|
||||
* zu Ende geführt. Danach wird der OS-Lock freigegeben.
|
||||
*/
|
||||
void stop();
|
||||
|
||||
@@ -85,15 +83,4 @@ public interface SchedulerControlUseCase {
|
||||
* @param seconds Intervall in Sekunden; sollte ≥ 30 sein
|
||||
*/
|
||||
void saveIntervalSeconds(int seconds);
|
||||
|
||||
/**
|
||||
* Deaktiviert den Autostart durch Persistieren von {@code scheduler.enabled=false}.
|
||||
* <p>
|
||||
* Wird vom Scheduler-Tab aufgerufen, wenn der Benutzer den fehlgeschlagenen
|
||||
* Autostart dauerhaft deaktivieren möchte. Sicher aufzurufen wenn der Scheduler
|
||||
* gestoppt ist.
|
||||
* <p>
|
||||
* Muss auf einem Hintergrund-Thread aufgerufen werden.
|
||||
*/
|
||||
void disableAutostart();
|
||||
}
|
||||
|
||||
+2
-3
@@ -3,9 +3,8 @@ package de.gecheckt.pdf.umbenenner.application.port.in;
|
||||
/**
|
||||
* Wird geworfen, wenn der Start des automatischen Schedulers fehlschlägt.
|
||||
* <p>
|
||||
* Mögliche Ursachen sind: Fehler beim Erwerb des Konfigurations-Datei-Locks,
|
||||
* Fehler beim Persistieren von {@code scheduler.enabled=true} oder
|
||||
* technische Fehler beim Starten des Scheduler-Adapters.
|
||||
* Mögliche Ursachen sind: Fehler beim Erwerb des Konfigurations-Datei-Locks
|
||||
* oder technische Fehler beim Starten des Scheduler-Adapters.
|
||||
* <p>
|
||||
* Diese Ausnahme ist ungeprüft (extends {@link RuntimeException}) und
|
||||
* wird in der Callchain bis zum GUI-Layer weitergeleitet, der eine
|
||||
|
||||
+4
-8
@@ -26,16 +26,13 @@ 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 autostartFailed {@code true}, wenn ein konfigurierter Autostart
|
||||
* beim Programmstart fehlgeschlagen ist
|
||||
*/
|
||||
public record SchedulerStatus(
|
||||
SchedulerState state,
|
||||
Optional<Instant> lastRunEndedAt,
|
||||
Optional<RunSummary> lastRunSummary,
|
||||
Optional<Instant> nextTickAt,
|
||||
Optional<String> lastError,
|
||||
boolean autostartFailed
|
||||
Optional<String> lastError
|
||||
) {
|
||||
|
||||
/**
|
||||
@@ -62,8 +59,8 @@ public record SchedulerStatus(
|
||||
/**
|
||||
* Erzeugt den initialen Scheduler-Status beim Programmstart.
|
||||
* <p>
|
||||
* Zustand ist {@link SchedulerState#STOPPED}, alle optionalen Felder
|
||||
* sind leer und {@code autostartFailed} ist {@code false}.
|
||||
* Zustand ist {@link SchedulerState#STOPPED} und alle optionalen Felder
|
||||
* sind leer.
|
||||
*
|
||||
* @return initialer Scheduler-Status
|
||||
*/
|
||||
@@ -73,8 +70,7 @@ public record SchedulerStatus(
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
false
|
||||
Optional.empty()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-14
@@ -3,23 +3,16 @@ package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
/**
|
||||
* Persistierte Scheduler-Einstellungen aus der {@code .properties}-Datei.
|
||||
* <p>
|
||||
* Dieses DTO repräsentiert die beiden Scheduler-Properties
|
||||
* {@code scheduler.enabled} und {@code scheduler.interval.seconds},
|
||||
* wie sie aus der Konfigurationsdatei gelesen werden. Es wird von
|
||||
* {@link SchedulerSettingsPort#loadSettings()} zurückgegeben und
|
||||
* dient als Eingabe für die Autostart-Entscheidung und die
|
||||
* Scheduler-Tab-Anzeige.
|
||||
* Dieses DTO repräsentiert die Scheduler-Property
|
||||
* {@code scheduler.interval.seconds}, wie sie aus der Konfigurationsdatei
|
||||
* gelesen wird. Es wird von {@link SchedulerSettingsPort#loadSettings()}
|
||||
* zurückgegeben und dient als Eingabe für die Scheduler-Tab-Anzeige.
|
||||
*
|
||||
* @param enabled {@code true}, wenn der Scheduler beim nächsten
|
||||
* Programmstart automatisch gestartet werden soll
|
||||
* @param intervalSeconds konfigurierte Wartezeit in Sekunden zwischen
|
||||
* Läufen; entspricht dem gelesenen Rohwert
|
||||
* ohne weitere Validierung
|
||||
*/
|
||||
public record SchedulerSettings(boolean enabled, int intervalSeconds) {
|
||||
|
||||
/** Standardwert für {@code scheduler.enabled}, wenn der Key fehlt oder leer ist. */
|
||||
public static final boolean DEFAULT_ENABLED = false;
|
||||
public record SchedulerSettings(int intervalSeconds) {
|
||||
|
||||
/** Standardwert für {@code scheduler.interval.seconds}, wenn der Key fehlt oder leer ist. */
|
||||
public static final int DEFAULT_INTERVAL_SECONDS = 180;
|
||||
@@ -27,9 +20,9 @@ public record SchedulerSettings(boolean enabled, int intervalSeconds) {
|
||||
/**
|
||||
* Erzeugt eine {@code SchedulerSettings}-Instanz mit Standardwerten.
|
||||
*
|
||||
* @return Instanz mit {@code enabled=false} und {@code intervalSeconds=180}
|
||||
* @return Instanz mit {@code intervalSeconds=180}
|
||||
*/
|
||||
public static SchedulerSettings defaults() {
|
||||
return new SchedulerSettings(DEFAULT_ENABLED, DEFAULT_INTERVAL_SECONDS);
|
||||
return new SchedulerSettings(DEFAULT_INTERVAL_SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-14
@@ -4,9 +4,8 @@ package de.gecheckt.pdf.umbenenner.application.port.out;
|
||||
* Outbound-Port zum Lesen und Schreiben der Scheduler-Einstellungen
|
||||
* in der {@code .properties}-Konfigurationsdatei.
|
||||
* <p>
|
||||
* Schreiboperationen aktualisieren ausschließlich die beiden
|
||||
* Scheduler-Keys ({@code scheduler.enabled} und
|
||||
* {@code scheduler.interval.seconds}). Alle übrigen Zeilen, Kommentare
|
||||
* Schreiboperationen aktualisieren ausschließlich den Scheduler-Key
|
||||
* {@code scheduler.interval.seconds}. Alle übrigen Zeilen, Kommentare
|
||||
* und unbekannten Properties bleiben unverändert erhalten.
|
||||
* <p>
|
||||
* Schreibvorgänge sind atomar: Sie erfolgen über eine temporäre Datei,
|
||||
@@ -33,17 +32,6 @@ public interface SchedulerSettingsPort {
|
||||
*/
|
||||
SchedulerSettings loadSettings();
|
||||
|
||||
/**
|
||||
* Schreibt den Wert von {@code scheduler.enabled} in die
|
||||
* Konfigurationsdatei.
|
||||
* <p>
|
||||
* Alle übrigen Inhalte der Datei bleiben unverändert.
|
||||
*
|
||||
* @param enabled neuer Wert für {@code scheduler.enabled}
|
||||
* @throws SchedulerSettingsWriteException wenn der Schreibvorgang fehlschlägt
|
||||
*/
|
||||
void saveEnabled(boolean enabled) throws SchedulerSettingsWriteException;
|
||||
|
||||
/**
|
||||
* Schreibt den Wert von {@code scheduler.interval.seconds} in die
|
||||
* Konfigurationsdatei.
|
||||
|
||||
+7
-62
@@ -28,7 +28,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
* Dieser Use Case:
|
||||
* <ul>
|
||||
* <li>Verwaltet den Scheduler-Lebenszyklus (Start, Stop) über einen
|
||||
* {@link SchedulerPort} und persistiert dabei den {@code enabled}-Wert.</li>
|
||||
* {@link SchedulerPort}.</li>
|
||||
* <li>Hält den exklusiven OS-Lock auf die Konfigurationsdatei über
|
||||
* {@link ConfigurationFileLockPort}, solange der Scheduler aktiv ist.</li>
|
||||
* <li>Liest beim Erstellen das konfigurierte Intervall via
|
||||
@@ -111,40 +111,28 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
|
||||
// Schritt 1: Zustand auf STARTING setzen
|
||||
statusRef.set(withState(current, SchedulerState.STARTING, Optional.empty()));
|
||||
|
||||
// Schritt 2: scheduler.enabled=true persistieren
|
||||
try {
|
||||
settingsPort.saveEnabled(true);
|
||||
} catch (Exception e) {
|
||||
logger.error("Scheduler konnte nicht gestartet werden: Einstellung nicht persistierbar.", e);
|
||||
statusRef.set(SchedulerStatus.initial());
|
||||
throw new SchedulerStartException(
|
||||
"Scheduler-Einstellung konnte nicht gespeichert werden.", e);
|
||||
}
|
||||
|
||||
// Schritt 3: OS-Lock erwerben
|
||||
// Schritt 2: OS-Lock erwerben
|
||||
try {
|
||||
lockPort.acquireLock();
|
||||
} catch (Exception e) {
|
||||
logger.error("Scheduler konnte nicht gestartet werden: Lock nicht erwerbbar.", e);
|
||||
tryPersistDisabled();
|
||||
statusRef.set(SchedulerStatus.initial());
|
||||
throw new SchedulerStartException(
|
||||
"Konfigurationsdatei konnte nicht gesperrt werden.", e);
|
||||
}
|
||||
|
||||
// Schritt 4: Scheduler-Adapter starten
|
||||
// Schritt 3: Scheduler-Adapter starten
|
||||
try {
|
||||
schedulerPort.startScheduler(new SchedulerConfig(intervalSeconds), this::executeWrappedTick);
|
||||
} catch (Exception e) {
|
||||
logger.error("Scheduler konnte nicht gestartet werden: Adapter-Start fehlgeschlagen.", e);
|
||||
lockPort.releaseLock();
|
||||
tryPersistDisabled();
|
||||
statusRef.set(SchedulerStatus.initial());
|
||||
throw new SchedulerStartException(
|
||||
"Scheduler-Adapter konnte nicht gestartet werden.", e);
|
||||
}
|
||||
|
||||
// Schritt 5: Zustand auf RUNNING_IDLE setzen
|
||||
// Schritt 4: Zustand auf RUNNING_IDLE setzen
|
||||
Instant nextTick = Instant.now().plusSeconds(intervalSeconds);
|
||||
statusRef.updateAndGet(s -> withState(s, SchedulerState.RUNNING_IDLE, Optional.of(nextTick)));
|
||||
logger.info("Scheduler gestartet. Intervall: {} Sekunden.", intervalSeconds);
|
||||
@@ -179,7 +167,6 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
|
||||
schedulerPort.stopScheduler();
|
||||
|
||||
if (!batchWasRunning) {
|
||||
tryPersistDisabled();
|
||||
lockPort.releaseLock();
|
||||
logger.info("Scheduler gestoppt.");
|
||||
} else {
|
||||
@@ -220,36 +207,6 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
|
||||
settingsPort.saveIntervalSeconds(seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deaktiviert den Autostart durch Persistieren von {@code scheduler.enabled=false}.
|
||||
*/
|
||||
@Override
|
||||
public void disableAutostart() {
|
||||
try {
|
||||
settingsPort.saveEnabled(false);
|
||||
logger.info("Autostart deaktiviert.");
|
||||
} catch (Exception e) {
|
||||
logger.warn("Fehler beim Deaktivieren des Autostarts.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Markiert den Autostart als fehlgeschlagen.
|
||||
* <p>
|
||||
* Wird von der Bootstrap-Schicht aufgerufen, wenn ein konfigurierter Autostart
|
||||
* beim Programmstart fehlgeschlagen ist. Alle übrigen Statusfelder bleiben erhalten;
|
||||
* lediglich {@link SchedulerStatus#autostartFailed()} wird auf {@code true} gesetzt.
|
||||
* <p>
|
||||
* Diese Methode darf nur aufgerufen werden, wenn der Scheduler noch gestoppt ist
|
||||
* (unmittelbar nach einem fehlgeschlagenen {@link #start()}).
|
||||
*/
|
||||
public void markAutostartFailed() {
|
||||
statusRef.updateAndGet(s -> new SchedulerStatus(
|
||||
s.state(), s.lastRunEndedAt(), s.lastRunSummary(),
|
||||
s.nextTickAt(), s.lastError(), true));
|
||||
logger.warn("Scheduler-Status: Autostart als fehlgeschlagen markiert.");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tick-Wrapper (package-private für Testbarkeit)
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -265,8 +222,7 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
|
||||
* <li>{@link SchedulerState#RUNNING_IDLE} bei normalem Weiterlauf</li>
|
||||
* <li>{@link SchedulerState#STOPPED} wenn ein Stopp-Befehl empfangen wurde</li>
|
||||
* </ul>
|
||||
* Im Fall {@link SchedulerState#STOPPED} werden außerdem {@code enabled=false}
|
||||
* persistiert und der OS-Lock freigegeben.
|
||||
* Im Fall {@link SchedulerState#STOPPED} wird außerdem der OS-Lock freigegeben.
|
||||
* <p>
|
||||
* Package-private, damit Unit-Tests den Tick-Wrapper direkt aufrufen können
|
||||
* ohne den Scheduler-Executor zu starten.
|
||||
@@ -315,11 +271,9 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
|
||||
lastRunEndedAt,
|
||||
lastRunSummary,
|
||||
nextTickAt,
|
||||
lastError,
|
||||
afterBatch.autostartFailed()));
|
||||
lastError));
|
||||
|
||||
if (stopping) {
|
||||
tryPersistDisabled();
|
||||
lockPort.releaseLock();
|
||||
logger.info("Scheduler gestoppt nach Abschluss des laufenden Batches.");
|
||||
}
|
||||
@@ -331,14 +285,6 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
|
||||
// Hilfsmethoden
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void tryPersistDisabled() {
|
||||
try {
|
||||
settingsPort.saveEnabled(false);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Fehler beim Zurücksetzen von scheduler.enabled auf false.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static SchedulerStatus withState(
|
||||
SchedulerStatus base, SchedulerState state, Optional<Instant> nextTickAt) {
|
||||
return new SchedulerStatus(
|
||||
@@ -346,7 +292,6 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
|
||||
base.lastRunEndedAt(),
|
||||
base.lastRunSummary(),
|
||||
nextTickAt,
|
||||
base.lastError(),
|
||||
base.autostartFailed());
|
||||
base.lastError());
|
||||
}
|
||||
}
|
||||
|
||||
+6
-133
@@ -4,7 +4,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -32,21 +31,9 @@ import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerConfig;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerSettings;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerSettingsPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.SchedulerSettingsWriteException;
|
||||
|
||||
/**
|
||||
* Unit-Tests für {@link DefaultSchedulerControlUseCase}.
|
||||
* <p>
|
||||
* Teststrategien:
|
||||
* <ul>
|
||||
* <li>Lifecycle-Tests (Start, Stop, Idempotenz) prüfen Zustandsübergänge
|
||||
* und Interaktionen mit gemockten Ports.</li>
|
||||
* <li>Tick-Tests rufen {@code executeWrappedTick()} direkt auf (package-private)
|
||||
* und verifizieren die Zustandsübergänge für alle drei
|
||||
* {@link BatchRunTriggerResult}-Varianten.</li>
|
||||
* <li>Rollback-Tests prüfen, dass fehlgeschlagene Startschritte den Zustand
|
||||
* vollständig auf {@link SchedulerState#STOPPED} zurücksetzen.</li>
|
||||
* </ul>
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DefaultSchedulerControlUseCaseTest {
|
||||
@@ -62,13 +49,9 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(false, 180));
|
||||
when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(180));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Initialer Zustand
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void getStatus_returnsInitialStatusOnCreation() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
@@ -80,15 +63,13 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
assertThat(status.lastRunSummary()).isEmpty();
|
||||
assertThat(status.nextTickAt()).isEmpty();
|
||||
assertThat(status.lastError()).isEmpty();
|
||||
assertThat(status.autostartFailed()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_loadsSettingsIntervalSeconds() {
|
||||
when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(false, 300));
|
||||
when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(300));
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
|
||||
// Verify interval is 300 by starting and checking the SchedulerConfig passed to schedulerPort
|
||||
useCase.start();
|
||||
ArgumentCaptor<SchedulerConfig> configCaptor = ArgumentCaptor.forClass(SchedulerConfig.class);
|
||||
verify(schedulerPort).startScheduler(configCaptor.capture(), any());
|
||||
@@ -97,7 +78,7 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
|
||||
@Test
|
||||
void constructor_clampsIntervalBelowMinimumTo30() {
|
||||
when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(false, 10));
|
||||
when(settingsPort.loadSettings()).thenReturn(new SchedulerSettings(10));
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
|
||||
useCase.start();
|
||||
@@ -106,10 +87,6 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
assertThat(configCaptor.getValue().intervalSeconds()).isEqualTo(30);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// start(): Normalfall
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void start_setsStateToRunningIdle() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
@@ -133,13 +110,6 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
assertThat(nextTickAt.get()).isBeforeOrEqualTo(after.plusSeconds(181));
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_persistsEnabledTrue() {
|
||||
createUseCase().start();
|
||||
|
||||
verify(settingsPort).saveEnabled(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_acquiresLock() {
|
||||
createUseCase().start();
|
||||
@@ -156,37 +126,14 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
assertThat(configCaptor.getValue().intervalSeconds()).isEqualTo(180);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// start(): Idempotenz
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void start_whenAlreadyActive_isIdempotent() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
useCase.start();
|
||||
|
||||
assertThatCode(useCase::start).doesNotThrowAnyException();
|
||||
// Second start must NOT persist, acquire lock or start scheduler again
|
||||
verify(settingsPort).saveEnabled(true); // only once
|
||||
verify(lockPort).acquireLock(); // only once
|
||||
verify(schedulerPort).startScheduler(any(), any()); // only once
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// start(): Rollback bei Fehlern
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void start_rollsBackToStoppedWhenSaveEnabledFails() {
|
||||
doThrow(new SchedulerSettingsWriteException("Schreibfehler"))
|
||||
.when(settingsPort).saveEnabled(true);
|
||||
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
|
||||
assertThatThrownBy(useCase::start).isInstanceOf(SchedulerStartException.class);
|
||||
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
|
||||
verify(lockPort, never()).acquireLock();
|
||||
verify(schedulerPort, never()).startScheduler(any(), any());
|
||||
verify(lockPort).acquireLock();
|
||||
verify(schedulerPort).startScheduler(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -198,7 +145,6 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
|
||||
assertThatThrownBy(useCase::start).isInstanceOf(SchedulerStartException.class);
|
||||
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
|
||||
verify(settingsPort).saveEnabled(false); // rollback
|
||||
verify(schedulerPort, never()).startScheduler(any(), any());
|
||||
}
|
||||
|
||||
@@ -212,27 +158,8 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
assertThatThrownBy(useCase::start).isInstanceOf(SchedulerStartException.class);
|
||||
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
|
||||
verify(lockPort).releaseLock();
|
||||
verify(settingsPort).saveEnabled(false); // rollback
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_rollbackPersistsEnabledFalseEvenWhenRollbackFails() {
|
||||
doThrow(new ConfigurationFileLockException("Timeout"))
|
||||
.when(lockPort).acquireLock();
|
||||
doThrow(new SchedulerSettingsWriteException("Rollback-Fehler"))
|
||||
.when(settingsPort).saveEnabled(false);
|
||||
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
|
||||
// Even if rollback persist fails, SchedulerStartException is thrown and state is STOPPED
|
||||
assertThatThrownBy(useCase::start).isInstanceOf(SchedulerStartException.class);
|
||||
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// stop(): Normalfall (kein laufender Batch)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void stop_whenRunningIdle_stopsImmediately() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
@@ -243,16 +170,6 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void stop_persistsEnabledFalse() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
useCase.start();
|
||||
|
||||
useCase.stop();
|
||||
|
||||
verify(settingsPort).saveEnabled(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void stop_releasesLock() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
@@ -283,17 +200,12 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
assertThat(useCase.getStatus().nextTickAt()).isEmpty();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// stop(): Idempotenz
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void stop_whenAlreadyStopped_isIdempotent() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
|
||||
assertThatCode(useCase::stop).doesNotThrowAnyException();
|
||||
verify(schedulerPort, never()).stopScheduler();
|
||||
verify(settingsPort, never()).saveEnabled(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -303,44 +215,28 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
useCase.stop();
|
||||
|
||||
assertThatCode(useCase::stop).doesNotThrowAnyException();
|
||||
verify(schedulerPort).stopScheduler(); // only once from the first stop
|
||||
verify(schedulerPort).stopScheduler();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// stop(): STOPPING_BATCH_ACTIVE-Szenario (via executeWrappedTick direkt)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void stop_whenBatchRunning_setsStoppingBatchActiveAndDefersCleaup() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
useCase.start();
|
||||
|
||||
// Während des triggerRun()-Aufrufs ist der Zustand RUNNING_BATCH_ACTIVE.
|
||||
// Ein dazwischen gerufenes stop() setzt STOPPING_BATCH_ACTIVE.
|
||||
// executeWrappedTick() erkennt das danach und setzt STOPPED.
|
||||
when(batchRunTrigger.triggerRun()).thenAnswer(invocation -> {
|
||||
// stop() während RUNNING_BATCH_ACTIVE → setzt STOPPING_BATCH_ACTIVE
|
||||
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.RUNNING_BATCH_ACTIVE);
|
||||
useCase.stop();
|
||||
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPING_BATCH_ACTIVE);
|
||||
// Lock und saveEnabled(false) dürfen hier noch nicht aufgerufen worden sein
|
||||
verify(lockPort, never()).releaseLock();
|
||||
verify(settingsPort, never()).saveEnabled(false);
|
||||
return new BatchRunTriggerResult.SkippedBusy();
|
||||
});
|
||||
|
||||
useCase.executeWrappedTick();
|
||||
|
||||
// Erst nach Abschluss des Batches: STOPPED und Cleanup
|
||||
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
|
||||
verify(lockPort).releaseLock();
|
||||
verify(settingsPort).saveEnabled(false);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// executeWrappedTick(): Zustandsübergänge für alle Ergebnisvarianten
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void tick_started_setsRunningIdleWithLastRunData() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
@@ -364,13 +260,11 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
void tick_started_clearsLastError() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
useCase.start();
|
||||
// Erst einen fehlgeschlagenen Tick erzeugen
|
||||
when(batchRunTrigger.triggerRun())
|
||||
.thenReturn(new BatchRunTriggerResult.Failed("GUI-Fehler", "technisch"));
|
||||
useCase.executeWrappedTick();
|
||||
assertThat(useCase.getStatus().lastError()).isPresent();
|
||||
|
||||
// Nun erfolgreich
|
||||
when(batchRunTrigger.triggerRun())
|
||||
.thenReturn(new BatchRunTriggerResult.Started(Instant.now(), RunSummary.noOp()));
|
||||
useCase.executeWrappedTick();
|
||||
@@ -397,13 +291,11 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
void tick_skippedBusy_preservesLastError() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
useCase.start();
|
||||
// Erst einen Fehler setzen
|
||||
when(batchRunTrigger.triggerRun())
|
||||
.thenReturn(new BatchRunTriggerResult.Failed("Vorheriger Fehler", "technisch"));
|
||||
useCase.executeWrappedTick();
|
||||
assertThat(useCase.getStatus().lastError()).isPresent();
|
||||
|
||||
// Dann SkippedBusy
|
||||
when(batchRunTrigger.triggerRun()).thenReturn(new BatchRunTriggerResult.SkippedBusy());
|
||||
useCase.executeWrappedTick();
|
||||
|
||||
@@ -431,7 +323,6 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
useCase.start();
|
||||
when(batchRunTrigger.triggerRun()).thenAnswer(invocation -> {
|
||||
// Innerhalb des Batch-Aufrufs muss der Zustand RUNNING_BATCH_ACTIVE sein
|
||||
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.RUNNING_BATCH_ACTIVE);
|
||||
return new BatchRunTriggerResult.SkippedBusy();
|
||||
});
|
||||
@@ -439,25 +330,12 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
useCase.executeWrappedTick();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// executeWrappedTick(): STOPPING_BATCH_ACTIVE → STOPPED
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void tick_whenStoppingBatchActive_setsStoppedAndCleansUp() {
|
||||
DefaultSchedulerControlUseCase useCase = createUseCase();
|
||||
useCase.start();
|
||||
|
||||
// Simuliere: stop() wurde aufgerufen während ein Batch lief.
|
||||
// stop() setzt den Zustand auf STOPPING_BATCH_ACTIVE wenn isBatchRunning().
|
||||
// Da kein echter Executor läuft, testen wir den Wrapper direkt:
|
||||
// Wir müssen den Zustand auf STOPPING_BATCH_ACTIVE bringen.
|
||||
// Dazu nutzen wir, dass executeWrappedTick() zuerst RUNNING_BATCH_ACTIVE setzt,
|
||||
// dann den Trigger aufruft. Wenn stop() dazwischen aufgerufen würde,
|
||||
// würde es STOPPING_BATCH_ACTIVE setzen.
|
||||
// Wir simulieren das, indem wir stop() innerhalb des triggerRun()-Aufrufs rufen:
|
||||
when(batchRunTrigger.triggerRun()).thenAnswer(invocation -> {
|
||||
// stop() während laufendem Batch aufrufen
|
||||
useCase.stop();
|
||||
return new BatchRunTriggerResult.Started(Instant.now(), RunSummary.noOp());
|
||||
});
|
||||
@@ -466,7 +344,6 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
|
||||
assertThat(useCase.getStatus().state()).isEqualTo(SchedulerState.STOPPED);
|
||||
assertThat(useCase.getStatus().nextTickAt()).isEmpty();
|
||||
verify(settingsPort).saveEnabled(false);
|
||||
verify(lockPort).releaseLock();
|
||||
}
|
||||
|
||||
@@ -489,10 +366,6 @@ class DefaultSchedulerControlUseCaseTest {
|
||||
assertThat(useCase.getStatus().lastRunSummary()).contains(summary);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Hilfsmethoden
|
||||
// =========================================================================
|
||||
|
||||
private DefaultSchedulerControlUseCase createUseCase() {
|
||||
return new DefaultSchedulerControlUseCase(schedulerPort, lockPort, settingsPort, batchRunTrigger);
|
||||
}
|
||||
|
||||
+3
-13
@@ -65,7 +65,6 @@ import de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunOutcome;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProcessingUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerControlUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerStartException;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.CreateNewDatabaseUseCase;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.HistoricalDocumentContext;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.in.ManualFileCopyRequest;
|
||||
@@ -1065,8 +1064,9 @@ public class BootstrapRunner {
|
||||
* <p>
|
||||
* Erzeugt {@link FileChannelConfigurationAccessAdapter}, {@link ScheduledExecutorServiceSchedulerAdapter}
|
||||
* und {@link DefaultSchedulerControlUseCase} und speichert den Use Case in
|
||||
* {@link #guiSchedulerUseCase}. Ist in der Konfiguration {@code scheduler.enabled=true}
|
||||
* gesetzt, wird der Scheduler sofort gestartet (Autostart).
|
||||
* {@link #guiSchedulerUseCase}. Der Scheduler wird beim Programmstart
|
||||
* niemals automatisch gestartet; der Benutzer startet ihn ausschließlich
|
||||
* bewusst über den Start-Button im Scheduler-Tab.
|
||||
* <p>
|
||||
* Schlägt die Initialisierung fehl, wird {@link #guiSchedulerUseCase} auf
|
||||
* {@link Optional#empty()} gesetzt und der Fehler als Warnung geloggt.
|
||||
@@ -1119,16 +1119,6 @@ public class BootstrapRunner {
|
||||
DefaultSchedulerControlUseCase schedulerUseCase = new DefaultSchedulerControlUseCase(
|
||||
schedulerAdapter, configAccessAdapter, configAccessAdapter, batchRunTrigger);
|
||||
guiSchedulerUseCase = Optional.of(schedulerUseCase);
|
||||
|
||||
if (configAccessAdapter.loadSettings().enabled()) {
|
||||
try {
|
||||
schedulerUseCase.start();
|
||||
LOG.info("Scheduler: Autostart aktiviert gemäß Konfiguration.");
|
||||
} catch (SchedulerStartException e) {
|
||||
LOG.warn("Scheduler: Autostart fehlgeschlagen: {}", e.getMessage());
|
||||
schedulerUseCase.markAutostartFailed();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Scheduler: Initialisierung fehlgeschlagen – Scheduler nicht verfügbar: {}",
|
||||
e.getMessage(), e);
|
||||
|
||||
Reference in New Issue
Block a user