1
0

Nachbearbeitung: Dokumentbezogene Persistenzfehler korrekt im

Batch-Ergebnis berücksichtigt
This commit is contained in:
2026-04-05 21:45:49 +02:00
parent 8f1e41c1a6
commit 9fd6bc469d
3 changed files with 155 additions and 31 deletions

View File

@@ -129,8 +129,10 @@ public class DocumentProcessingCoordinator {
* must not be null * must not be null
* @param attemptStart the instant at which processing of this candidate began; * @param attemptStart the instant at which processing of this candidate began;
* must not be null * must not be null
* @return true if processing and persistence succeeded for this document, false if a
* persistence failure occurred
*/ */
public void process( public boolean process(
SourceDocumentCandidate candidate, SourceDocumentCandidate candidate,
DocumentFingerprint fingerprint, DocumentFingerprint fingerprint,
DocumentProcessingOutcome outcome, DocumentProcessingOutcome outcome,
@@ -143,7 +145,7 @@ public class DocumentProcessingCoordinator {
Objects.requireNonNull(context, "context must not be null"); Objects.requireNonNull(context, "context must not be null");
Objects.requireNonNull(attemptStart, "attemptStart must not be null"); Objects.requireNonNull(attemptStart, "attemptStart must not be null");
processDeferredOutcome(candidate, fingerprint, context, attemptStart, ignored -> outcome); return processDeferredOutcome(candidate, fingerprint, context, attemptStart, ignored -> outcome);
} }
/** /**
@@ -172,8 +174,10 @@ public class DocumentProcessingCoordinator {
* must not be null * must not be null
* @param pipelineExecutor functional interface that executes the extraction and pre-check * @param pipelineExecutor functional interface that executes the extraction and pre-check
* pipeline when needed; must not be null * pipeline when needed; must not be null
* @return true if processing and persistence succeeded for this document, false if a
* persistence failure occurred (lookup, attempt write, or record write)
*/ */
public void processDeferredOutcome( public boolean processDeferredOutcome(
SourceDocumentCandidate candidate, SourceDocumentCandidate candidate,
DocumentFingerprint fingerprint, DocumentFingerprint fingerprint,
BatchRunContext context, BatchRunContext context,
@@ -194,16 +198,16 @@ public class DocumentProcessingCoordinator {
if (lookupResult instanceof PersistenceLookupTechnicalFailure failure) { if (lookupResult instanceof PersistenceLookupTechnicalFailure failure) {
logger.error("Cannot process '{}': master record lookup failed: {}", logger.error("Cannot process '{}': master record lookup failed: {}",
candidate.uniqueIdentifier(), failure.errorMessage()); candidate.uniqueIdentifier(), failure.errorMessage());
return; return false;
} }
// Step 3: Determine the action based on the lookup result // Step 3: Determine the action based on the lookup result
switch (lookupResult) { return switch (lookupResult) {
case DocumentTerminalSuccess terminalSuccess -> { case DocumentTerminalSuccess terminalSuccess -> {
// Document already successfully processed → skip // Document already successfully processed → skip
logger.info("Skipping '{}': already successfully processed (fingerprint: {}).", logger.info("Skipping '{}': already successfully processed (fingerprint: {}).",
candidate.uniqueIdentifier(), fingerprint.sha256Hex()); candidate.uniqueIdentifier(), fingerprint.sha256Hex());
persistSkipAttempt( yield persistSkipAttempt(
candidate, fingerprint, terminalSuccess.record(), candidate, fingerprint, terminalSuccess.record(),
ProcessingStatus.SKIPPED_ALREADY_PROCESSED, ProcessingStatus.SKIPPED_ALREADY_PROCESSED,
context, attemptStart); context, attemptStart);
@@ -213,7 +217,7 @@ public class DocumentProcessingCoordinator {
// Document finally failed → skip // Document finally failed → skip
logger.info("Skipping '{}': already finally failed (fingerprint: {}).", logger.info("Skipping '{}': already finally failed (fingerprint: {}).",
candidate.uniqueIdentifier(), fingerprint.sha256Hex()); candidate.uniqueIdentifier(), fingerprint.sha256Hex());
persistSkipAttempt( yield persistSkipAttempt(
candidate, fingerprint, terminalFailure.record(), candidate, fingerprint, terminalFailure.record(),
ProcessingStatus.SKIPPED_FINAL_FAILURE, ProcessingStatus.SKIPPED_FINAL_FAILURE,
context, attemptStart); context, attemptStart);
@@ -222,22 +226,24 @@ public class DocumentProcessingCoordinator {
case DocumentUnknown ignored -> { case DocumentUnknown ignored -> {
// New document execute pipeline and process // New document execute pipeline and process
DocumentProcessingOutcome outcome = pipelineExecutor.apply(candidate); DocumentProcessingOutcome outcome = pipelineExecutor.apply(candidate);
processAndPersistNewDocument(candidate, fingerprint, outcome, context, attemptStart); yield processAndPersistNewDocument(candidate, fingerprint, outcome, context, attemptStart);
} }
case DocumentKnownProcessable knownProcessable -> { case DocumentKnownProcessable knownProcessable -> {
// Known but not terminal execute pipeline and process // Known but not terminal execute pipeline and process
DocumentProcessingOutcome outcome = pipelineExecutor.apply(candidate); DocumentProcessingOutcome outcome = pipelineExecutor.apply(candidate);
processAndPersistKnownDocument( yield processAndPersistKnownDocument(
candidate, fingerprint, outcome, knownProcessable.record(), candidate, fingerprint, outcome, knownProcessable.record(),
context, attemptStart); context, attemptStart);
} }
default -> default -> {
// Exhaustive sealed hierarchy; this branch is unreachable // Exhaustive sealed hierarchy; this branch is unreachable
logger.error("Unexpected lookup result type for '{}': {}", logger.error("Unexpected lookup result type for '{}': {}",
candidate.uniqueIdentifier(), lookupResult.getClass().getSimpleName()); candidate.uniqueIdentifier(), lookupResult.getClass().getSimpleName());
yield false;
} }
};
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -257,8 +263,9 @@ public class DocumentProcessingCoordinator {
* or {@link ProcessingStatus#SKIPPED_FINAL_FAILURE}) * or {@link ProcessingStatus#SKIPPED_FINAL_FAILURE})
* @param context the current batch run context * @param context the current batch run context
* @param attemptStart the start instant of this processing attempt * @param attemptStart the start instant of this processing attempt
* @return true if persistence succeeded, false if a persistence exception occurred
*/ */
private void persistSkipAttempt( private boolean persistSkipAttempt(
SourceDocumentCandidate candidate, SourceDocumentCandidate candidate,
DocumentFingerprint fingerprint, DocumentFingerprint fingerprint,
DocumentRecord existingRecord, DocumentRecord existingRecord,
@@ -293,10 +300,12 @@ public class DocumentProcessingCoordinator {
logger.debug("Skip attempt #{} persisted for '{}' with status {}.", logger.debug("Skip attempt #{} persisted for '{}' with status {}.",
attemptNumber, candidate.uniqueIdentifier(), skipStatus); attemptNumber, candidate.uniqueIdentifier(), skipStatus);
return true;
} catch (DocumentPersistenceException e) { } catch (DocumentPersistenceException e) {
logger.error("Failed to persist skip attempt for '{}': {}", logger.error("Failed to persist skip attempt for '{}': {}",
candidate.uniqueIdentifier(), e.getMessage(), e); candidate.uniqueIdentifier(), e.getMessage(), e);
return false;
} }
} }
@@ -305,7 +314,7 @@ public class DocumentProcessingCoordinator {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
/** Maps the pipeline outcome for a new document and persists attempt + new master record. */ /** Maps the pipeline outcome for a new document and persists attempt + new master record. */
private void processAndPersistNewDocument( private boolean processAndPersistNewDocument(
SourceDocumentCandidate candidate, SourceDocumentCandidate candidate,
DocumentFingerprint fingerprint, DocumentFingerprint fingerprint,
DocumentProcessingOutcome pipelineOutcome, DocumentProcessingOutcome pipelineOutcome,
@@ -315,7 +324,7 @@ public class DocumentProcessingCoordinator {
Instant now = Instant.now(); Instant now = Instant.now();
ProcessingOutcomeTransition.ProcessingOutcome outcome = mapOutcomeForNewDocument(pipelineOutcome); ProcessingOutcomeTransition.ProcessingOutcome outcome = mapOutcomeForNewDocument(pipelineOutcome);
DocumentRecord newRecord = buildNewDocumentRecord(fingerprint, candidate, outcome, now); DocumentRecord newRecord = buildNewDocumentRecord(fingerprint, candidate, outcome, now);
persistAttemptAndRecord(candidate, fingerprint, context, attemptStart, now, outcome, return persistAttemptAndRecord(candidate, fingerprint, context, attemptStart, now, outcome,
txOps -> txOps.createDocumentRecord(newRecord)); txOps -> txOps.createDocumentRecord(newRecord));
} }
@@ -324,7 +333,7 @@ public class DocumentProcessingCoordinator {
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
/** Maps the pipeline outcome for a known document and persists attempt + updated master record. */ /** Maps the pipeline outcome for a known document and persists attempt + updated master record. */
private void processAndPersistKnownDocument( private boolean processAndPersistKnownDocument(
SourceDocumentCandidate candidate, SourceDocumentCandidate candidate,
DocumentFingerprint fingerprint, DocumentFingerprint fingerprint,
DocumentProcessingOutcome pipelineOutcome, DocumentProcessingOutcome pipelineOutcome,
@@ -335,7 +344,7 @@ public class DocumentProcessingCoordinator {
Instant now = Instant.now(); Instant now = Instant.now();
ProcessingOutcomeTransition.ProcessingOutcome outcome = mapOutcomeForKnownDocument(pipelineOutcome, existingRecord.failureCounters()); ProcessingOutcomeTransition.ProcessingOutcome outcome = mapOutcomeForKnownDocument(pipelineOutcome, existingRecord.failureCounters());
DocumentRecord updatedRecord = buildUpdatedDocumentRecord(existingRecord, candidate, outcome, now); DocumentRecord updatedRecord = buildUpdatedDocumentRecord(existingRecord, candidate, outcome, now);
persistAttemptAndRecord(candidate, fingerprint, context, attemptStart, now, outcome, return persistAttemptAndRecord(candidate, fingerprint, context, attemptStart, now, outcome,
txOps -> txOps.updateDocumentRecord(updatedRecord)); txOps -> txOps.updateDocumentRecord(updatedRecord));
} }
@@ -440,8 +449,10 @@ public class DocumentProcessingCoordinator {
* {@code recordWriter} performs either {@code createDocumentRecord} or * {@code recordWriter} performs either {@code createDocumentRecord} or
* {@code updateDocumentRecord} depending on whether the document is new or known. * {@code updateDocumentRecord} depending on whether the document is new or known.
* All persistence failures are caught and logged; the batch run continues. * All persistence failures are caught and logged; the batch run continues.
*
* @return true if persistence succeeded, false if a persistence exception occurred
*/ */
private void persistAttemptAndRecord( private boolean persistAttemptAndRecord(
SourceDocumentCandidate candidate, SourceDocumentCandidate candidate,
DocumentFingerprint fingerprint, DocumentFingerprint fingerprint,
BatchRunContext context, BatchRunContext context,
@@ -465,10 +476,12 @@ public class DocumentProcessingCoordinator {
outcome.overallStatus(), outcome.overallStatus(),
outcome.counters().contentErrorCount(), outcome.counters().contentErrorCount(),
outcome.counters().transientErrorCount()); outcome.counters().transientErrorCount());
return true;
} catch (DocumentPersistenceException e) { } catch (DocumentPersistenceException e) {
logger.error("Failed to persist processing result for '{}': {}", logger.error("Failed to persist processing result for '{}': {}",
candidate.uniqueIdentifier(), e.getMessage(), e); candidate.uniqueIdentifier(), e.getMessage(), e);
return false;
} }
} }

View File

@@ -158,9 +158,14 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
/** /**
* Loads candidates and processes them one by one. * Loads candidates and processes them one by one.
* <p>
* Tracks whether any document-level persistence failures occur during processing.
* A persistence failure for a single document causes the overall batch outcome
* to be FAILURE instead of SUCCESS.
* *
* @param context the current batch run context * @param context the current batch run context
* @return SUCCESS if all candidates were processed, FAILURE if source access fails * @return SUCCESS if all candidates were processed without persistence failures,
* FAILURE if source access fails or any document-level persistence failure occurred
*/ */
private BatchRunOutcome processCandidates(BatchRunContext context) { private BatchRunOutcome processCandidates(BatchRunContext context) {
List<SourceDocumentCandidate> candidates; List<SourceDocumentCandidate> candidates;
@@ -172,13 +177,24 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
} }
logger.info("Found {} PDF candidate(s) in source folder.", candidates.size()); logger.info("Found {} PDF candidate(s) in source folder.", candidates.size());
// Track whether any document-level persistence failures occurred
boolean anyPersistenceFailure = false;
// Process each candidate // Process each candidate
for (SourceDocumentCandidate candidate : candidates) { for (SourceDocumentCandidate candidate : candidates) {
processCandidate(candidate, context); if (!processCandidate(candidate, context)) {
anyPersistenceFailure = true;
}
} }
logger.info("Batch run completed. Processed {} candidate(s). RunId: {}", logger.info("Batch run completed. Processed {} candidate(s). RunId: {}",
candidates.size(), context.runId()); candidates.size(), context.runId());
if (anyPersistenceFailure) {
logger.warn("Batch run completed with document-level persistence failure(s).");
return BatchRunOutcome.FAILURE;
}
return BatchRunOutcome.SUCCESS; return BatchRunOutcome.SUCCESS;
} }
@@ -209,7 +225,7 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
* <li>Record the attempt start instant.</li> * <li>Record the attempt start instant.</li>
* <li>Compute the SHA-256 fingerprint of the candidate file content.</li> * <li>Compute the SHA-256 fingerprint of the candidate file content.</li>
* <li>If fingerprint computation fails: log as non-identifiable run event and * <li>If fingerprint computation fails: log as non-identifiable run event and
* return — no SQLite record is created.</li> * return true — no SQLite record is created, but no persistence failure occurred.</li>
* <li>Load document master record.</li> * <li>Load document master record.</li>
* <li>If already {@code SUCCESS} → persist skip attempt with * <li>If already {@code SUCCESS} → persist skip attempt with
* {@code SKIPPED_ALREADY_PROCESSED}.</li> * {@code SKIPPED_ALREADY_PROCESSED}.</li>
@@ -226,21 +242,23 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
* *
* @param candidate the candidate to process * @param candidate the candidate to process
* @param context the current batch run context * @param context the current batch run context
* @return true if the candidate was processed without persistence failures (fingerprint
* errors return true; persistence failures return false)
*/ */
private void processCandidate(SourceDocumentCandidate candidate, BatchRunContext context) { private boolean processCandidate(SourceDocumentCandidate candidate, BatchRunContext context) {
logger.debug("Processing candidate: {}", candidate.uniqueIdentifier()); logger.debug("Processing candidate: {}", candidate.uniqueIdentifier());
Instant attemptStart = Instant.now(); Instant attemptStart = Instant.now();
FingerprintResult fingerprintResult = fingerprintPort.computeFingerprint(candidate); FingerprintResult fingerprintResult = fingerprintPort.computeFingerprint(candidate);
switch (fingerprintResult) { return switch (fingerprintResult) {
case FingerprintTechnicalError fingerprintError -> { case FingerprintTechnicalError fingerprintError -> {
handleFingerprintError(candidate, fingerprintError); handleFingerprintError(candidate, fingerprintError);
yield true; // fingerprint errors are not persistence failures
} }
case FingerprintSuccess fingerprintSuccess -> { case FingerprintSuccess fingerprintSuccess ->
handleFingerprintSuccess(candidate, fingerprintSuccess, context, attemptStart); handleFingerprintSuccess(candidate, fingerprintSuccess, context, attemptStart);
} };
}
} }
/** /**
@@ -264,8 +282,9 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
* @param fingerprintSuccess the successful fingerprint result * @param fingerprintSuccess the successful fingerprint result
* @param context the batch run context * @param context the batch run context
* @param attemptStart the instant when processing started * @param attemptStart the instant when processing started
* @return true if processing and persistence succeeded, false if a persistence failure occurred
*/ */
private void handleFingerprintSuccess( private boolean handleFingerprintSuccess(
SourceDocumentCandidate candidate, SourceDocumentCandidate candidate,
FingerprintSuccess fingerprintSuccess, FingerprintSuccess fingerprintSuccess,
BatchRunContext context, BatchRunContext context,
@@ -274,7 +293,7 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa
logger.debug("Fingerprint computed for '{}': {}", logger.debug("Fingerprint computed for '{}': {}",
candidate.uniqueIdentifier(), fingerprint.sha256Hex()); candidate.uniqueIdentifier(), fingerprint.sha256Hex());
documentProcessingCoordinator.processDeferredOutcome( return documentProcessingCoordinator.processDeferredOutcome(
candidate, candidate,
fingerprint, fingerprint,
context, context,

View File

@@ -424,6 +424,98 @@ class BatchRunProcessingUseCaseTest {
assertEquals(3, processor.processCallCount(), "processor should be called once per candidate"); assertEquals(3, processor.processCallCount(), "processor should be called once per candidate");
} }
// -------------------------------------------------------------------------
// Document-level persistence failure handling
// -------------------------------------------------------------------------
/**
* Regression test: when a document-level persistence failure occurs,
* the batch outcome must be FAILURE, not SUCCESS.
*/
@Test
void execute_documentPersistenceFailure_batchOutcomeIsFailure() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort();
RuntimeConfiguration config = buildConfig(tempDir);
SourceDocumentCandidate candidate = makeCandidate("document.pdf");
PdfExtractionSuccess success = new PdfExtractionSuccess("Invoice text", new PdfPageCount(1));
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(candidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(success);
// Use a coordinator that always fails persistence
DocumentProcessingCoordinator failingProcessor = new DocumentProcessingCoordinator(
new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(),
new NoOpUnitOfWorkPort(), new NoOpProcessingLogger()) {
@Override
public boolean processDeferredOutcome(
de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate candidate,
de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint fingerprint,
de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext context,
java.time.Instant attemptStart,
java.util.function.Function<de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate, de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome> pipelineExecutor) {
// Always report persistence failure
return false;
}
};
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, candidatesPort, extractionPort,
new AlwaysSuccessFingerprintPort(), failingProcessor);
BatchRunContext context = new BatchRunContext(new RunId("persist-fail"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isFailure(), "Document persistence failure should yield FAILURE outcome");
assertFalse(outcome.isSuccess(), "Batch must not succeed when document persistence failed");
}
/**
* Regression test: mixed batch where one document succeeds and one has persistence failure.
* The batch outcome must be FAILURE due to the persistence failure.
*/
@Test
void execute_mixedBatch_oneCandidateSuccess_oneDocumentPersistenceFails_batchIsFailure() throws Exception {
MockRunLockPort lockPort = new MockRunLockPort();
RuntimeConfiguration config = buildConfig(tempDir);
SourceDocumentCandidate goodCandidate = makeCandidate("good.pdf");
SourceDocumentCandidate failCandidate = makeCandidate("fails.pdf");
PdfExtractionSuccess success = new PdfExtractionSuccess("Invoice text", new PdfPageCount(1));
FixedCandidatesPort candidatesPort = new FixedCandidatesPort(List.of(goodCandidate, failCandidate));
FixedExtractionPort extractionPort = new FixedExtractionPort(success);
// Coordinator that succeeds for first document, fails persistence for second
DocumentProcessingCoordinator selectiveFailingProcessor = new DocumentProcessingCoordinator(
new NoOpDocumentRecordRepository(), new NoOpProcessingAttemptRepository(),
new NoOpUnitOfWorkPort(), new NoOpProcessingLogger()) {
private int callCount = 0;
@Override
public boolean processDeferredOutcome(
de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate candidate,
de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint fingerprint,
de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext context,
java.time.Instant attemptStart,
java.util.function.Function<de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate, de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome> pipelineExecutor) {
callCount++;
// First document succeeds, second fails persistence
return callCount == 1;
}
};
DefaultBatchRunProcessingUseCase useCase = buildUseCase(
config, lockPort, candidatesPort, extractionPort,
new AlwaysSuccessFingerprintPort(), selectiveFailingProcessor);
BatchRunContext context = new BatchRunContext(new RunId("mixed-persist-fail"), Instant.now());
BatchRunOutcome outcome = useCase.execute(context);
assertTrue(outcome.isFailure(),
"Batch must fail when any document has a persistence failure, even if others succeeded");
assertFalse(outcome.isSuccess(), "Cannot be SUCCESS when persistence failed for any document");
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Helpers // Helpers
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -615,7 +707,7 @@ class BatchRunProcessingUseCaseTest {
} }
@Override @Override
public void process( public boolean process(
de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate candidate, de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate candidate,
de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint fingerprint, de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint fingerprint,
de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome outcome, de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome outcome,
@@ -623,11 +715,11 @@ class BatchRunProcessingUseCaseTest {
java.time.Instant attemptStart) { java.time.Instant attemptStart) {
processCallCount++; processCallCount++;
// Delegate to super so the real logic runs (with no-op repos) // Delegate to super so the real logic runs (with no-op repos)
super.process(candidate, fingerprint, outcome, context, attemptStart); return super.process(candidate, fingerprint, outcome, context, attemptStart);
} }
@Override @Override
public void processDeferredOutcome( public boolean processDeferredOutcome(
de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate candidate, de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate candidate,
de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint fingerprint, de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint fingerprint,
de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext context, de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext context,
@@ -635,7 +727,7 @@ class BatchRunProcessingUseCaseTest {
java.util.function.Function<de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate, de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome> pipelineExecutor) { java.util.function.Function<de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate, de.gecheckt.pdf.umbenenner.domain.model.DocumentProcessingOutcome> pipelineExecutor) {
processCallCount++; processCallCount++;
// Delegate to super so the real logic runs (with no-op repos) // Delegate to super so the real logic runs (with no-op repos)
super.processDeferredOutcome(candidate, fingerprint, context, attemptStart, pipelineExecutor); return super.processDeferredOutcome(candidate, fingerprint, context, attemptStart, pipelineExecutor);
} }
int processCallCount() { return processCallCount; } int processCallCount() { return processCallCount; }