Implementierung für M2 vorläufig abgeschlossen
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user