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:
@@ -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>
|
||||
|
||||
+121
-1
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user