1
0

Implementierung für M2 vorläufig abgeschlossen

This commit is contained in:
2026-03-31 21:52:48 +02:00
parent 301f1acf08
commit 9d66a446b3
29 changed files with 1892 additions and 52 deletions

View File

@@ -0,0 +1,82 @@
package de.gecheckt.pdf.umbenenner.adapter.outbound.lock;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
/**
* File-based implementation of {@link RunLockPort} that uses a lock file to prevent concurrent runs.
* <p>
* AP-006 Implementation: 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.
* <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.
*/
public class FilesystemRunLockPortAdapter implements RunLockPort {
private static final Logger LOG = LogManager.getLogger(FilesystemRunLockPortAdapter.class);
private final Path lockFile;
/**
* Creates a new FilesystemRunLockPortAdapter for the given lock file path.
*
* @param lockFile path of the lock file to create on acquire and delete on release
*/
public FilesystemRunLockPortAdapter(Path lockFile) {
this.lockFile = lockFile;
}
/**
* Acquires the run lock by creating the lock file.
* <p>
* If the lock file already exists, throws {@link RunLockUnavailableException}.
* If the parent directory does not exist, it is created before attempting file creation.
*
* @throws RunLockUnavailableException if the lock file already exists or cannot be created
*/
@Override
public void acquire() {
if (Files.exists(lockFile)) {
throw new RunLockUnavailableException(
"Run lock file already exists - another instance may be running: " + lockFile);
}
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 acquired: {} (PID {})", lockFile, pid);
} catch (IOException e) {
throw new RunLockUnavailableException("Failed to acquire run lock file: " + lockFile, e);
}
}
/**
* Releases the run lock by deleting the lock file.
* <p>
* If deletion fails, a warning is logged but no exception is thrown.
*/
@Override
public void release() {
try {
boolean deleted = Files.deleteIfExists(lockFile);
if (deleted) {
LOG.debug("Run lock released: {}", lockFile);
}
} catch (IOException e) {
LOG.warn("Failed to release run lock file: {} — manual cleanup may be required", lockFile, e);
}
}
}

View File

@@ -0,0 +1,14 @@
/**
* Outbound adapter for run lock management.
* <p>
* Components:
* <ul>
* <li>{@link de.gecheckt.pdf.umbenenner.adapter.outbound.lock.FilesystemRunLockPortAdapter}
* — File-based run lock that prevents concurrent instances (AP-006)</li>
* </ul>
* <p>
* AP-006: Uses atomic file creation ({@code CREATE_NEW}) to establish an exclusive lock.
* Stores the acquiring process PID in the lock file for diagnostics.
* Release is best-effort and logs a warning on failure without throwing.
*/
package de.gecheckt.pdf.umbenenner.adapter.outbound.lock;

View File

@@ -0,0 +1,122 @@
package de.gecheckt.pdf.umbenenner.adapter.outbound.lock;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for {@link FilesystemRunLockPortAdapter}.
* <p>
* Tests cover acquire/release success path, lock contention behavior,
* and robust release behavior.
*/
class FilesystemRunLockPortAdapterTest {
@TempDir
Path tempDir;
@Test
void acquire_createsLockFile() {
Path lockFile = tempDir.resolve("test.lock");
FilesystemRunLockPortAdapter adapter = new FilesystemRunLockPortAdapter(lockFile);
adapter.acquire();
assertTrue(Files.exists(lockFile), "Lock file should exist after acquire");
}
@Test
void acquire_writesProcessPidToLockFile() throws Exception {
Path lockFile = tempDir.resolve("test.lock");
FilesystemRunLockPortAdapter adapter = new FilesystemRunLockPortAdapter(lockFile);
adapter.acquire();
String content = Files.readString(lockFile);
long pid = ProcessHandle.current().pid();
assertEquals(String.valueOf(pid), content, "Lock file should contain current PID");
}
@Test
void release_deletesLockFile() throws Exception {
Path lockFile = tempDir.resolve("test.lock");
FilesystemRunLockPortAdapter adapter = new FilesystemRunLockPortAdapter(lockFile);
adapter.acquire();
assertTrue(Files.exists(lockFile));
adapter.release();
assertFalse(Files.exists(lockFile), "Lock file should be deleted after release");
}
@Test
void release_doesNotThrowIfLockFileAbsent() {
Path lockFile = tempDir.resolve("nonexistent.lock");
FilesystemRunLockPortAdapter adapter = new FilesystemRunLockPortAdapter(lockFile);
assertDoesNotThrow(adapter::release, "Release without prior acquire should not throw");
}
@Test
void acquire_throwsRunLockUnavailableExceptionIfLockFileExists() throws Exception {
Path lockFile = tempDir.resolve("test.lock");
Files.writeString(lockFile, "12345");
FilesystemRunLockPortAdapter adapter = new FilesystemRunLockPortAdapter(lockFile);
assertThrows(RunLockUnavailableException.class, adapter::acquire,
"Acquire should throw RunLockUnavailableException when lock file already exists");
}
@Test
void acquire_exceptionMessageContainsLockFilePath() throws Exception {
Path lockFile = tempDir.resolve("test.lock");
Files.writeString(lockFile, "12345");
FilesystemRunLockPortAdapter adapter = new FilesystemRunLockPortAdapter(lockFile);
RunLockUnavailableException ex = assertThrows(RunLockUnavailableException.class, adapter::acquire);
assertTrue(ex.getMessage().contains(lockFile.toString()),
"Exception message should contain lock file path");
}
@Test
void acquire_createsParentDirectoriesIfAbsent() {
Path lockFile = tempDir.resolve("nested").resolve("deep").resolve("test.lock");
FilesystemRunLockPortAdapter adapter = new FilesystemRunLockPortAdapter(lockFile);
adapter.acquire();
assertTrue(Files.exists(lockFile), "Lock file should be created including parent dirs");
}
@Test
void acquireAndRelease_canBeRepeated() {
Path lockFile = tempDir.resolve("test.lock");
FilesystemRunLockPortAdapter adapter = new FilesystemRunLockPortAdapter(lockFile);
adapter.acquire();
adapter.release();
adapter.acquire();
adapter.release();
assertFalse(Files.exists(lockFile), "Lock file should not exist after second release");
}
@Test
void acquire_secondCallThrowsRunLockUnavailableException() {
Path lockFile = tempDir.resolve("test.lock");
FilesystemRunLockPortAdapter adapter = new FilesystemRunLockPortAdapter(lockFile);
adapter.acquire();
assertThrows(RunLockUnavailableException.class, adapter::acquire,
"Second acquire without release should throw RunLockUnavailableException");
}
}