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
@@ -18,6 +18,7 @@ import de.gecheckt.pdf.umbenenner.adapter.in.gui.history.GuiHistoryResetDocument
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorState;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationEditorStateFactory;
import de.gecheckt.pdf.umbenenner.application.port.in.ResetDocumentStatusResult;
import de.gecheckt.pdf.umbenenner.application.port.in.SchedulerControlUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.AiModelCatalogPort;
import de.gecheckt.pdf.umbenenner.application.port.out.modelcatalog.EffectiveApiKeyDescriptor;
import de.gecheckt.pdf.umbenenner.application.validation.editor.ApiKeyResolutionPort;
@@ -48,14 +49,19 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
* the {@link GuiManualFileCopyPort} used to manually copy a source file to the target
* folder for documents that have not yet been successfully processed, and
* the {@link GuiHistoricalDocumentContextPort} used to retrieve the historical processing
* context for documents that were skipped in the current run, and the resolved application
* version string that the status bar displays at the bottom of the main window.
* context for documents that were skipped in the current run, the resolved application
* version string that the status bar displays at the bottom of the main window, and the
* optional {@link SchedulerControlUseCase} for controlling the automatic scheduler.
* <p>
* The optional {@code applicationContextError} carries a human-readable German error
* message when the bootstrap-side application run context could not be initialised at
* startup (e.g., invalid or incomplete configuration). An empty value signals that the
* run context was built successfully and batch runs can be launched immediately.
* <p>
* The optional {@code schedulerControlUseCase} is present when the automatic scheduler
* was successfully wired at startup. An empty value means scheduler control is not
* available in this startup context (e.g., no valid configuration was loaded at startup).
* <p>
* All ports and services are supplied by Bootstrap so that the GUI adapter does not need to
* know about provider-specific HTTP details or adapter wiring.
*/
@@ -84,7 +90,8 @@ public record GuiStartupContext(
GuiDeleteDocumentHistoryPort deleteDocumentHistoryPort,
GuiPromptEditorPortFactory promptEditorPortFactory,
GuiCreateNewDatabasePort createNewDatabasePort,
Optional<String> applicationContextError) {
Optional<String> applicationContextError,
Optional<SchedulerControlUseCase> schedulerControlUseCase) {
/**
* Creates a fully wired startup context.
@@ -167,6 +174,75 @@ public record GuiStartupContext(
"promptEditorPortFactory must not be null");
createNewDatabasePort = Objects.requireNonNull(createNewDatabasePort,
"createNewDatabasePort must not be null");
schedulerControlUseCase = schedulerControlUseCase == null ? Optional.empty() : schedulerControlUseCase;
}
/**
* Backward-compatible constructor that fills {@code schedulerControlUseCase} with
* {@link Optional#empty()}.
* <p>
* Preserves existing callers that were written before the scheduler was added.
*
* @param initialState initial editor state; must not be {@code null}
* @param startupNotice optional startup notice; {@code null} becomes empty
* @param configurationFileLoader file-loading callback; must not be {@code null}
* @param configurationFileWriter file-writing callback; must not be {@code null}
* @param modelCatalogPort port for retrieving AI model lists; must not be {@code null}
* @param apiKeyResolutionPort port for resolving API key provenance; must not be {@code null}
* @param providerTechnicalTestService service for provider-specific technical checks; must not be {@code null}
* @param pathCheckPort port for filesystem path accessibility checks; must not be {@code null}
* @param technicalTestOrchestrator orchestrator for the full technical test run; must not be {@code null}
* @param correctionExecutionService service for executing confirmed corrective actions; must not be {@code null}
* @param batchRunLauncher bridge that executes a regular batch run; must not be {@code null}
* @param miniRunLauncher bridge that executes a targeted mini-run; must not be {@code null}
* @param resetDocumentStatusPort bridge that resets document status; must not be {@code null}
* @param manualFileRenamePort bridge that renames a target file; must not be {@code null}
* @param manualFileCopyPort bridge that copies a source file; must not be {@code null}
* @param historicalDocumentContextPort bridge for historical processing context; must not be {@code null}
* @param applicationVersion resolved application version string; {@code null} defaults to {@code "dev"}
* @param promptEditorPort bridge zum Prompt-Editor-Use-Case; must not be {@code null}
* @param historyOverviewPort bridge for history overview; must not be {@code null}
* @param historyDetailsPort bridge for history details; must not be {@code null}
* @param historyResetDocumentStatusPort bridge for history reset; must not be {@code null}
* @param deleteDocumentHistoryPort bridge for history deletion; must not be {@code null}
* @param promptEditorPortFactory factory for prompt editor ports; must not be {@code null}
* @param createNewDatabasePort bridge for new database creation; must not be {@code null}
* @param applicationContextError optional error from context init; {@code null} becomes empty
*/
public GuiStartupContext(
GuiConfigurationEditorState initialState,
Optional<String> startupNotice,
GuiConfigurationFileLoader configurationFileLoader,
GuiConfigurationFileWriter configurationFileWriter,
AiModelCatalogPort modelCatalogPort,
ApiKeyResolutionPort apiKeyResolutionPort,
ProviderTechnicalTestService providerTechnicalTestService,
PathCheckPort pathCheckPort,
TechnicalTestOrchestrator technicalTestOrchestrator,
CorrectionExecutionService correctionExecutionService,
GuiBatchRunLauncher batchRunLauncher,
GuiMiniRunLauncher miniRunLauncher,
GuiResetDocumentStatusPort resetDocumentStatusPort,
GuiManualFileRenamePort manualFileRenamePort,
GuiManualFileCopyPort manualFileCopyPort,
GuiHistoricalDocumentContextPort historicalDocumentContextPort,
String applicationVersion,
GuiPromptEditorPort promptEditorPort,
GuiHistoryOverviewPort historyOverviewPort,
GuiHistoryDetailsPort historyDetailsPort,
GuiHistoryResetDocumentStatusPort historyResetDocumentStatusPort,
GuiDeleteDocumentHistoryPort deleteDocumentHistoryPort,
GuiPromptEditorPortFactory promptEditorPortFactory,
GuiCreateNewDatabasePort createNewDatabasePort,
Optional<String> applicationContextError) {
this(initialState, startupNotice, configurationFileLoader, configurationFileWriter,
modelCatalogPort, apiKeyResolutionPort, providerTechnicalTestService, pathCheckPort,
technicalTestOrchestrator, correctionExecutionService, batchRunLauncher,
miniRunLauncher, resetDocumentStatusPort, manualFileRenamePort, manualFileCopyPort,
historicalDocumentContextPort, applicationVersion, promptEditorPort,
historyOverviewPort, historyDetailsPort, historyResetDocumentStatusPort,
deleteDocumentHistoryPort, promptEditorPortFactory, createNewDatabasePort,
applicationContextError, Optional.empty());
}
/**
@@ -197,6 +197,23 @@ public class DefaultSchedulerControlUseCase implements SchedulerControlUseCase {
return statusRef.get();
}
/**
* 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)
// -------------------------------------------------------------------------
+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.