Scheduler-Ports und Typen im Application-Modul anlegen (Schritt 3)
Neue Typen (port/in): - SchedulerControlUseCase – Inbound-Port: start(), stop(), getStatus() - SchedulerState – Enum: STOPPED, STARTING, RUNNING_IDLE, RUNNING_BATCH_ACTIVE, STOPPING_BATCH_ACTIVE - SchedulerStatus – Immutable Record mit AtomicReference-ready Snapshot - SchedulerStartException – Unchecked Exception für Start-Fehler Neue Typen (port/out): - RunLockHandle – AutoCloseable für tryAcquire() in try-with-resources - RunSummary – Aggregierte Lauf-Ergebniszähler (success/failed/skipped) - BatchRunTrigger – @FunctionalInterface für synchronen Lauf-Trigger - BatchRunTriggerResult – Sealed Interface: Started, SkippedBusy, Failed - SchedulerConfig – Betriebskonfiguration (intervalSeconds >= 30) - SchedulerSettings – Persistierte Properties-Werte mit Defaults - SchedulerPort – startScheduler() / stopScheduler() - ConfigurationFileLockPort – acquireLock() / releaseLock() / isLocked() - ConfigurationFileLockException – Unchecked bei Lock-Erwerb-Fehler - SchedulerSettingsPort – loadSettings() / saveEnabled() / saveIntervalSeconds() - SchedulerSettingsWriteException – Unchecked bei Schreib-Fehler Erweiterungen: - RunLockPort: neue Methode tryAcquire() → Optional<RunLockHandle> - FilesystemRunLockPortAdapter: implementiert tryAcquire() atomar via CREATE_NEW; idempotentes Handle via AtomicBoolean Test-Fixes: - 9 Mock-Klassen in application- und bootstrap-Tests um tryAcquire() ergänzt (liefern Optional.empty(), da nur blockierender Pfad getestet) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+84
-19
@@ -4,22 +4,26 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
|
||||
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException;
|
||||
|
||||
/**
|
||||
* File-based implementation of {@link RunLockPort} that uses a lock file to prevent concurrent runs.
|
||||
* Dateibasierte Implementierung von {@link RunLockPort}.
|
||||
* <p>
|
||||
* Creates an exclusive lock file on acquire and deletes it on release.
|
||||
* If the lock file already exists, {@link #acquire()} throws {@link RunLockUnavailableException}
|
||||
* to signal that another instance is already running.
|
||||
* Verwendet eine Lock-Datei, um parallele Läufe zu verhindern.
|
||||
* Beim Erwerb wird die Lock-Datei angelegt; bei der Freigabe wird sie gelöscht.
|
||||
* Existiert die Datei bereits, ist der Lock belegt.
|
||||
* <p>
|
||||
* The lock file contains the PID of the acquiring process. Release is best-effort: a failure
|
||||
* to delete the lock file is logged as a warning but does not throw.
|
||||
* Die Lock-Datei enthält die PID des erwerbenden Prozesses.
|
||||
* Die Freigabe ist best-effort: Ein Fehler beim Löschen wird als Warnung
|
||||
* geloggt, wirft aber keine Ausnahme.
|
||||
*/
|
||||
public class FilesystemRunLockPortAdapter implements RunLockPort {
|
||||
|
||||
@@ -28,27 +32,31 @@ public class FilesystemRunLockPortAdapter implements RunLockPort {
|
||||
private final Path lockFile;
|
||||
|
||||
/**
|
||||
* Creates a new FilesystemRunLockPortAdapter for the given lock file path.
|
||||
* Erstellt einen neuen {@code FilesystemRunLockPortAdapter} für den
|
||||
* angegebenen Lock-Datei-Pfad.
|
||||
*
|
||||
* @param lockFile path of the lock file to create on acquire and delete on release
|
||||
* @param lockFile Pfad der Lock-Datei, die beim Erwerb angelegt und
|
||||
* bei der Freigabe gelöscht wird
|
||||
*/
|
||||
public FilesystemRunLockPortAdapter(Path lockFile) {
|
||||
this.lockFile = lockFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires the run lock by creating the lock file.
|
||||
* Erwirbt den Run-Lock durch Anlegen der Lock-Datei (blockierend).
|
||||
* <p>
|
||||
* If the lock file already exists, throws {@link RunLockUnavailableException}.
|
||||
* If the parent directory does not exist, it is created before attempting file creation.
|
||||
* Existiert die Lock-Datei bereits, wird eine
|
||||
* {@link RunLockUnavailableException} geworfen. Das übergeordnete
|
||||
* Verzeichnis wird bei Bedarf angelegt.
|
||||
*
|
||||
* @throws RunLockUnavailableException if the lock file already exists or cannot be created
|
||||
* @throws RunLockUnavailableException wenn die Lock-Datei bereits existiert
|
||||
* oder nicht angelegt werden kann
|
||||
*/
|
||||
@Override
|
||||
public void acquire() {
|
||||
if (Files.exists(lockFile)) {
|
||||
throw new RunLockUnavailableException(
|
||||
"Run lock file already exists - another instance may be running: " + lockFile);
|
||||
"Run-Lock-Datei existiert bereits – eine andere Instanz könnte laufen: " + lockFile);
|
||||
}
|
||||
try {
|
||||
Path parent = lockFile.getParent();
|
||||
@@ -57,26 +65,83 @@ public class FilesystemRunLockPortAdapter implements RunLockPort {
|
||||
}
|
||||
long pid = ProcessHandle.current().pid();
|
||||
Files.writeString(lockFile, String.valueOf(pid), StandardOpenOption.CREATE_NEW);
|
||||
LOG.debug("Run lock acquired: {} (PID {})", lockFile, pid);
|
||||
LOG.debug("Run-Lock erworben: {} (PID {})", lockFile, pid);
|
||||
} catch (IOException e) {
|
||||
throw new RunLockUnavailableException("Failed to acquire run lock file: " + lockFile, e);
|
||||
throw new RunLockUnavailableException("Run-Lock-Datei konnte nicht angelegt werden: " + lockFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the run lock by deleting the lock file.
|
||||
* Gibt den Run-Lock durch Löschen der Lock-Datei frei.
|
||||
* <p>
|
||||
* If deletion fails, a warning is logged but no exception is thrown.
|
||||
* Schlägt das Löschen fehl, wird eine Warnung geloggt; keine Ausnahme
|
||||
* wird geworfen.
|
||||
*/
|
||||
@Override
|
||||
public void release() {
|
||||
try {
|
||||
boolean deleted = Files.deleteIfExists(lockFile);
|
||||
if (deleted) {
|
||||
LOG.debug("Run lock released: {}", lockFile);
|
||||
LOG.debug("Run-Lock freigegeben: {}", lockFile);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to release run lock file: {} — manual cleanup may be required", lockFile, e);
|
||||
LOG.warn("Run-Lock-Datei konnte nicht gelöscht werden: {} – manuelle Bereinigung erforderlich",
|
||||
lockFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Versucht nicht-blockierend, den Run-Lock zu erwerben.
|
||||
* <p>
|
||||
* Existiert die Lock-Datei bereits, wird sofort {@link Optional#empty()}
|
||||
* zurückgegeben. Andernfalls wird die Datei atomar mit
|
||||
* {@link StandardOpenOption#CREATE_NEW} angelegt. Schlägt das Anlegen
|
||||
* aufgrund einer Race-Condition fehl (z.B. gleichzeitiger Erwerb durch
|
||||
* eine andere Instanz), wird ebenfalls {@link Optional#empty()} zurückgegeben.
|
||||
* <p>
|
||||
* Das zurückgegebene {@link RunLockHandle} gibt den Lock idempotent frei.
|
||||
*
|
||||
* @return Handle mit dem erworbenen Lock, oder {@link Optional#empty()}
|
||||
* wenn der Lock nicht verfügbar ist
|
||||
*/
|
||||
@Override
|
||||
public Optional<RunLockHandle> tryAcquire() {
|
||||
if (Files.exists(lockFile)) {
|
||||
LOG.debug("Run-Lock nicht verfügbar (Datei existiert): {}", lockFile);
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
Path parent = lockFile.getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
long pid = ProcessHandle.current().pid();
|
||||
Files.writeString(lockFile, String.valueOf(pid), StandardOpenOption.CREATE_NEW);
|
||||
LOG.debug("Run-Lock (tryAcquire) erworben: {} (PID {})", lockFile, pid);
|
||||
return Optional.of(new FilesystemRunLockHandle());
|
||||
} catch (IOException e) {
|
||||
// CREATE_NEW schlägt mit FileAlreadyExistsException fehl wenn eine
|
||||
// Race-Condition vorliegt – kein Fehler, sondern normaler Busy-Zustand
|
||||
LOG.debug("Run-Lock (tryAcquire) nicht verfügbar: {} – {}", lockFile, e.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle für einen über {@link #tryAcquire()} erworbenen Run-Lock.
|
||||
* <p>
|
||||
* Gibt den Lock idempotent frei. Mehrfaches Aufrufen von {@link #close()}
|
||||
* hat nach dem ersten Aufruf keine Wirkung.
|
||||
*/
|
||||
private class FilesystemRunLockHandle implements RunLockHandle {
|
||||
|
||||
private final AtomicBoolean released = new AtomicBoolean(false);
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (released.compareAndSet(false, true)) {
|
||||
FilesystemRunLockPortAdapter.this.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user