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:
+79
-3
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+17
@@ -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)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -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