Compare commits

...

2 Commits

Author SHA1 Message Date
marcus c137d9e02e Fix #61: Connection-Leak in SqliteUnitOfWorkAdapter beheben
Connection wird jetzt in try-with-resources geoeffnet, sodass sie
auch dann zuverlaessig geschlossen wird, wenn setAutoCommit(false) wirft.
Rollback-Behandlung bleibt unveraendert innerhalb des inneren catch-Blocks.
Ebenfalls: korrekten Import fuer DateTimeFormatter ergaenzt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 15:52:21 +02:00
marcus ea8b94acc7 Fix #57 Fix #58: Catch auf DocumentPersistenceException einengen
Catch in ResolveHistoricalDocumentContext und ResolveHistoricalFileName
praezisiert: nur DocumentPersistenceException wird abgefangen, geloggt
(WARN) und mit leerem Optional beantwortet. Andere RuntimeExceptions
propagieren weiter zum Aufrufer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 15:52:13 +02:00
4 changed files with 55 additions and 58 deletions
@@ -7,7 +7,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.time.DateTimeFormatter;
import java.time.format.DateTimeFormatter;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
@@ -41,58 +41,51 @@ public class SqliteUnitOfWorkAdapter implements UnitOfWorkPort {
public void executeInTransaction(Consumer<TransactionOperations> operations) {
Objects.requireNonNull(operations, "operations must not be null");
Connection connection = null;
try {
connection = DriverManager.getConnection(jdbcUrl);
try (Connection connection = DriverManager.getConnection(jdbcUrl)) {
connection.setAutoCommit(false);
TransactionOperationsImpl txOps = new TransactionOperationsImpl(connection);
operations.accept(txOps);
try {
TransactionOperationsImpl txOps = new TransactionOperationsImpl(connection);
operations.accept(txOps);
connection.commit();
logger.debug("Transaction committed successfully");
connection.commit();
logger.debug("Transaktion erfolgreich abgeschlossen.");
} catch (DocumentPersistenceException e) {
// Datenbankfehler auf Dokumentebene: Rollback, dann weiterpropagieren
try {
connection.rollback();
logger.debug("Transaktion zurückgerollt (Dokumentfehler): {}", e.getMessage());
} catch (SQLException rollbackEx) {
logger.error("Rollback fehlgeschlagen: {}", rollbackEx.getMessage(), rollbackEx);
}
throw e;
} catch (RuntimeException e) {
// Unerwarteter Laufzeitfehler: Rollback, dann als Persistenzfehler weitergeben
try {
connection.rollback();
logger.debug("Transaktion zurückgerollt (Laufzeitfehler): {}", e.getMessage());
} catch (SQLException rollbackEx) {
logger.error("Rollback fehlgeschlagen: {}", rollbackEx.getMessage(), rollbackEx);
}
throw new DocumentPersistenceException("Transaktion fehlgeschlagen: " + e.getMessage(), e);
} catch (SQLException e) {
// SQL-Fehler innerhalb der Transaktion: Rollback, dann als Persistenzfehler weitergeben
try {
connection.rollback();
logger.debug("Transaktion zurückgerollt (SQL-Fehler): {}", e.getMessage());
} catch (SQLException rollbackEx) {
logger.error("Rollback fehlgeschlagen: {}", rollbackEx.getMessage(), rollbackEx);
}
throw new DocumentPersistenceException("Transaktion fehlgeschlagen: " + e.getMessage(), e);
}
} catch (DocumentPersistenceException e) {
// Re-throw document-level persistence errors as-is, but still rollback
if (connection != null) {
try {
connection.rollback();
logger.debug("Transaction rolled back due to document error: {}", e.getMessage());
} catch (SQLException rollbackEx) {
logger.error("Failed to rollback transaction: {}", rollbackEx.getMessage(), rollbackEx);
}
}
throw e;
} catch (RuntimeException e) {
// Rollback on any RuntimeException and wrap in DocumentPersistenceException
if (connection != null) {
try {
connection.rollback();
logger.debug("Transaction rolled back due to error: {}", e.getMessage());
} catch (SQLException rollbackEx) {
logger.error("Failed to rollback transaction: {}", rollbackEx.getMessage(), rollbackEx);
}
}
throw new DocumentPersistenceException("Transaction failed: " + e.getMessage(), e);
} catch (SQLException e) {
// Rollback for any SQL error
if (connection != null) {
try {
connection.rollback();
logger.debug("Transaction rolled back due to error: {}", e.getMessage());
} catch (SQLException rollbackEx) {
logger.error("Failed to rollback transaction: {}", rollbackEx.getMessage(), rollbackEx);
}
}
throw new DocumentPersistenceException("Transaction failed: " + e.getMessage(), e);
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
logger.warn("Failed to close connection: {}", e.getMessage(), e);
}
}
// Verbindungsaufbau oder setAutoCommit(false) fehlgeschlagen
throw new DocumentPersistenceException(
"Datenbankverbindung konnte nicht hergestellt werden: " + e.getMessage(), e);
}
}
@@ -8,6 +8,7 @@ import org.apache.logging.log4j.Logger;
import de.gecheckt.pdf.umbenenner.application.port.in.HistoricalDocumentContext;
import de.gecheckt.pdf.umbenenner.application.port.in.ResolveHistoricalDocumentContextUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordLookupResult;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalFinalFailure;
@@ -28,10 +29,11 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
* <li>In allen anderen Fällen (unbekannt, verarbeitbar) sowie bei erwarteten technischen
* Abfragefehlern: leeres {@link Optional}.</li>
* </ul>
* Unerwartete {@link RuntimeException}s aus dem Repository werden geloggt (WARN) und
* weiterpropagiert. Erwartete Lookup-Fehler werden als
* {@link DocumentPersistenceException}s aus dem Repository werden geloggt (WARN) und
* führen zu einem leeren {@link Optional}. Andere unerwartete Laufzeitfehler
* propagieren zum Aufrufer. Erwartete Lookup-Fehler werden als
* {@code PersistenceLookupTechnicalFailure} im Rückgabewert kodiert und führen
* zu einem leeren {@link Optional}.
* ebenfalls zu einem leeren {@link Optional}.
*/
public class DefaultResolveHistoricalDocumentContextUseCase
implements ResolveHistoricalDocumentContextUseCase {
@@ -84,10 +86,10 @@ public class DefaultResolveHistoricalDocumentContextUseCase
failure.record().lastFailureInstant()));
}
return Optional.empty();
} catch (RuntimeException e) {
logger.warn("Unerwarteter Fehler beim Lesen des Dokument-Stammsatzes für Fingerprint {}: {}",
} catch (DocumentPersistenceException e) {
logger.warn("Persistenzfehler beim Lesen des Dokument-Stammsatzes für Fingerprint {}: {}",
fingerprint, e.getMessage(), e);
throw e;
return Optional.empty();
}
}
}
@@ -7,6 +7,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import de.gecheckt.pdf.umbenenner.application.port.in.ResolveHistoricalFileNameUseCase;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordLookupResult;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecordRepository;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalSuccess;
@@ -21,10 +22,11 @@ import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
* <p>
* Für alle anderen terminalen Zustände oder wenn kein Stammsatz vorhanden ist,
* wird ein leeres {@link Optional} zurückgegeben.
* Unerwartete {@link RuntimeException}s aus dem Repository werden geloggt (WARN) und
* weiterpropagiert. Erwartete Lookup-Fehler werden als
* {@link DocumentPersistenceException}s aus dem Repository werden geloggt (WARN) und
* führen zu einem leeren {@link Optional}. Andere unerwartete Laufzeitfehler
* propagieren zum Aufrufer. Erwartete Lookup-Fehler werden als
* {@code PersistenceLookupTechnicalFailure} im Rückgabewert kodiert und führen
* zu einem leeren {@link Optional}.
* ebenfalls zu einem leeren {@link Optional}.
*/
public class DefaultResolveHistoricalFileNameUseCase implements ResolveHistoricalFileNameUseCase {
@@ -69,10 +71,10 @@ public class DefaultResolveHistoricalFileNameUseCase implements ResolveHistorica
return Optional.ofNullable(success.record().lastTargetFileName());
}
return Optional.empty();
} catch (RuntimeException e) {
logger.warn("Unerwarteter Fehler beim Lesen des historischen Dateinamens für Fingerprint {}: {}",
} catch (DocumentPersistenceException e) {
logger.warn("Persistenzfehler beim Lesen des historischen Dateinamens für Fingerprint {}: {}",
fingerprint, e.getMessage(), e);
throw e;
return Optional.empty();
}
}
}