Bootstrap-Wiring: Scheduler in GUI-Startkontext verdrahten

- pdf-umbenenner-bootstrap/pom.xml: Abhängigkeit auf adapter-in-scheduler hinzugefügt
- GuiStartupContext: neues Feld schedulerControlUseCase (Optional<SchedulerControlUseCase>)
  als 26. Record-Komponente; 25-Parameter-Backward-Compat-Konstruktor sichert Abwärtskompatibilität
- DefaultSchedulerControlUseCase: öffentliche Methode markAutostartFailed() ergänzt
- BootstrapRunner: guiSchedulerUseCase-Feld, tryInitializeScheduler(), stopGuiSchedulerIfActive()
  sowie BatchRunTrigger-Lambda; Autostart gemäß scheduler.enabled-Konfiguration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 14:42:35 +02:00
parent 434c882d7d
commit d66364e254
4 changed files with 222 additions and 4 deletions
+5
View File
@@ -36,6 +36,11 @@
<artifactId>pdf-umbenenner-adapter-out</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>de.gecheckt</groupId>
<artifactId>pdf-umbenenner-adapter-in-scheduler</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Logging -->
<dependency>
@@ -18,6 +18,8 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import de.gecheckt.pdf.umbenenner.adapter.in.cli.SchedulerBatchCommand;
import de.gecheckt.pdf.umbenenner.adapter.in.scheduler.FileChannelConfigurationAccessAdapter;
import de.gecheckt.pdf.umbenenner.adapter.in.scheduler.ScheduledExecutorServiceSchedulerAdapter;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiAdapter;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationFileLoader;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationFileWriter;
@@ -61,6 +63,8 @@ import de.gecheckt.pdf.umbenenner.application.config.provider.ProviderConfigurat
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;
@@ -73,6 +77,9 @@ import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
import de.gecheckt.pdf.umbenenner.application.port.in.ResolveHistoricalDocumentContextUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.ActiveDatabaseContextPort;
import de.gecheckt.pdf.umbenenner.application.port.out.AiContentSensitivity;
import de.gecheckt.pdf.umbenenner.application.port.out.BatchRunTrigger;
import de.gecheckt.pdf.umbenenner.application.port.out.BatchRunTriggerResult;
import de.gecheckt.pdf.umbenenner.application.port.out.RunSummary;
import de.gecheckt.pdf.umbenenner.application.port.out.AiInvocationPort;
import de.gecheckt.pdf.umbenenner.application.port.out.ClockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.DatabaseCreationPort;
@@ -100,6 +107,7 @@ import de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryResetDocument
import de.gecheckt.pdf.umbenenner.adapter.out.sqlite.SqliteHistoryQueryAdapter;
import de.gecheckt.pdf.umbenenner.application.port.out.history.HistoryQueryPort;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultBatchRunProcessingUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultSchedulerControlUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultCreateNewDatabaseUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultDeleteDocumentHistoryUseCase;
import de.gecheckt.pdf.umbenenner.application.usecase.DefaultHistoryDetailsUseCase;
@@ -246,6 +254,17 @@ public class BootstrapRunner {
*/
private volatile Optional<ApplicationRunContext> guiApplicationRunContext = Optional.empty();
/**
* Der Scheduler-Use-Case, der beim GUI-Start mit einer gültigen Konfiguration
* initialisiert wird. Enthält den {@link DefaultSchedulerControlUseCase}, der den
* automatischen Scheduler-Lebenszyklus verwaltet.
* <p>
* Wird beim Start via {@link #tryInitializeScheduler(Path)} gesetzt. Leer, wenn
* beim Start keine gültige Konfiguration vorlag oder die Scheduler-Initialisierung
* fehlschlug. {@code volatile} sichert die Sichtbarkeit über Thread-Grenzen hinweg.
*/
private volatile Optional<DefaultSchedulerControlUseCase> guiSchedulerUseCase = Optional.empty();
/**
* Functional interface encapsulating the legacy configuration migration step.
* <p>
@@ -761,9 +780,28 @@ public class BootstrapRunner {
} catch (Exception e) {
LOG.error("GUI startup failed: {}", e.getMessage(), e);
return 1;
} finally {
stopGuiSchedulerIfActive();
}
}
/**
* Stoppt den aktiven Scheduler, wenn die GUI beendet wird.
* <p>
* Wird im {@code finally}-Block von {@link #startGuiMode(Optional)} aufgerufen,
* damit der Scheduler-Thread ordentlich beendet wird, bevor der JVM-Prozess endet.
* Ist kein Scheduler aktiv oder bereits gestoppt, hat dieser Aufruf keine Wirkung.
*/
private void stopGuiSchedulerIfActive() {
guiSchedulerUseCase.ifPresent(scheduler -> {
if (scheduler.getStatus().state().isActive()) {
LOG.info("GUI-Beendigung: laufender Scheduler wird gestoppt.");
scheduler.stop();
}
});
guiSchedulerUseCase = Optional.empty();
}
/**
* Runs the headless batch processing pipeline for the given startup arguments.
* <p>
@@ -936,13 +974,19 @@ public class BootstrapRunner {
GuiPromptEditorPort promptEditorPort = buildGuiPromptEditorPort(
loadedState.values().promptTemplateFile());
Optional<String> contextError = initializeApplicationRunContext(configPath);
if (contextError.isEmpty()) {
tryInitializeScheduler(configPath);
}
Optional<SchedulerControlUseCase> schedulerUseCase =
guiSchedulerUseCase.map(s -> (SchedulerControlUseCase) s);
return new GuiStartupContext(loadedState, Optional.empty(), loader, writer,
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
miniRunLauncher, resetPort, manualRenamePort, manualCopyPort,
historicalDocumentContextPort, applicationVersion, promptEditorPort,
historyOverviewPort, historyDetailsPort, historyResetPort, deleteHistoryPort,
this::buildGuiPromptEditorPort, createNewDatabasePort, contextError);
this::buildGuiPromptEditorPort, createNewDatabasePort, contextError,
schedulerUseCase);
} catch (GuiConfigurationLoadException e) {
LOG.error("GUI startup: configuration could not be loaded, starting without it: {}",
e.getMessage(), e);
@@ -975,6 +1019,82 @@ public class BootstrapRunner {
}
}
/**
* Initialisiert den automatischen Scheduler für den GUI-Betrieb.
* <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).
* <p>
* Schlägt die Initialisierung fehl, wird {@link #guiSchedulerUseCase} auf
* {@link Optional#empty()} gesetzt und der Fehler als Warnung geloggt.
* Der GUI-Start wird dadurch nicht abgebrochen.
*
* @param configFilePath Pfad zur Konfigurationsdatei; muss auf Platte existieren
*/
private void tryInitializeScheduler(Path configFilePath) {
try {
FileChannelConfigurationAccessAdapter configAccessAdapter =
new FileChannelConfigurationAccessAdapter(configFilePath);
ScheduledExecutorServiceSchedulerAdapter schedulerAdapter =
new ScheduledExecutorServiceSchedulerAdapter(
result -> LOG.debug("Scheduler-Tick-Ergebnis: {}",
result.getClass().getSimpleName()));
BatchRunTrigger batchRunTrigger = () -> {
Optional<ApplicationRunContext> ctxOpt = guiApplicationRunContext;
if (ctxOpt.isEmpty()) {
return new BatchRunTriggerResult.Failed(
"Kein Anwendungskontext verfügbar.",
"guiApplicationRunContext ist leer Scheduler-Tick übersprungen.");
}
ApplicationRunContext ctx = ctxOpt.get();
try {
RunLockPort runLockPort = runLockPortFactory.create(
resolveLockFilePath(ctx.startConfiguration()));
BatchRunContext runContext = createRunContext();
BatchRunProcessingUseCase useCase = buildProductionBatchUseCase(
ctx.startConfiguration(), runLockPort,
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunProgressObserver.noOp(),
de.gecheckt.pdf.umbenenner.application.port.in.BatchRunCancellationToken.neverCancelled());
BatchRunOutcome outcome = useCase.execute(runContext);
runContext.setEndInstant(Instant.now());
if (outcome.isLockUnavailable()) {
return new BatchRunTriggerResult.SkippedBusy();
} else if (outcome.isSuccess()) {
return new BatchRunTriggerResult.Started(Instant.now(), RunSummary.noOp());
} else {
return new BatchRunTriggerResult.Failed(
"Verarbeitungslauf fehlgeschlagen.",
"BatchRunOutcome: " + outcome);
}
} catch (RuntimeException e) {
LOG.error("Scheduler-Tick: Unerwarteter Fehler: {}", e.getMessage(), e);
return new BatchRunTriggerResult.Failed(
"Unerwarteter Fehler: " + e.getMessage(),
e.getClass().getSimpleName());
}
};
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);
guiSchedulerUseCase = Optional.empty();
}
}
/**
* Erzeugt einen vollständig verdrahteten {@link GuiPromptEditorPort} für den angegebenen
* Prompt-Dateipfad.