M7 Zentrale Retry-Entscheidung vervollständigt und vereinheitlicht
This commit is contained in:
+30
-24
@@ -33,6 +33,7 @@ import de.gecheckt.pdf.umbenenner.domain.model.NamingProposalReady;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
@@ -538,12 +539,11 @@ public class DocumentProcessingCoordinator {
|
||||
* Persists a transient error for a document-level technical failure during the
|
||||
* target-copy finalization stage.
|
||||
* <p>
|
||||
* The resulting status is {@link ProcessingStatus#FAILED_FINAL} if the incremented
|
||||
* transient error counter reaches {@code maxRetriesTransient}; otherwise
|
||||
* {@link ProcessingStatus#FAILED_RETRYABLE}. The transient error counter is always
|
||||
* incremented by exactly one. This method does not increment the laufübergreifenden
|
||||
* transient counter for the within-run immediate retry — only the combined outcome
|
||||
* of the retry is reported here.
|
||||
* The retry decision (status and updated counters) is derived via the central
|
||||
* rule in {@link ProcessingOutcomeTransition}, keeping the target-copy finalization
|
||||
* path consistent with the AI pipeline path. The transient error counter is always
|
||||
* incremented by exactly one. This method does not count the within-run immediate
|
||||
* retry — only the combined outcome of the retry is reported here.
|
||||
*
|
||||
* @return true if the error was persisted; false if the error persistence itself failed
|
||||
*/
|
||||
@@ -556,12 +556,16 @@ public class DocumentProcessingCoordinator {
|
||||
Instant now,
|
||||
String errorMessage) {
|
||||
|
||||
FailureCounters updatedCounters =
|
||||
existingRecord.failureCounters().withIncrementedTransientErrorCount();
|
||||
boolean limitReached = updatedCounters.transientErrorCount() >= maxRetriesTransient;
|
||||
ProcessingStatus errorStatus = limitReached
|
||||
? ProcessingStatus.FAILED_FINAL
|
||||
: ProcessingStatus.FAILED_RETRYABLE;
|
||||
// Delegate to the central retry rule so the target-copy path and the AI pipeline
|
||||
// path are governed by the same logic without duplication.
|
||||
ProcessingOutcomeTransition.ProcessingOutcome transition =
|
||||
ProcessingOutcomeTransition.forKnownDocument(
|
||||
new TechnicalDocumentError(candidate, errorMessage, null),
|
||||
existingRecord.failureCounters(),
|
||||
maxRetriesTransient);
|
||||
FailureCounters updatedCounters = transition.counters();
|
||||
ProcessingStatus errorStatus = transition.overallStatus();
|
||||
boolean retryable = transition.retryable();
|
||||
|
||||
try {
|
||||
int attemptNumber = processingAttemptRepository.loadNextAttemptNumber(fingerprint);
|
||||
@@ -569,7 +573,7 @@ public class DocumentProcessingCoordinator {
|
||||
fingerprint, context.runId(), attemptNumber, attemptStart, now,
|
||||
errorStatus,
|
||||
errorStatus.name(),
|
||||
errorMessage, !limitReached);
|
||||
errorMessage, retryable);
|
||||
|
||||
DocumentRecord errorRecord = buildTransientErrorRecord(
|
||||
existingRecord, candidate, updatedCounters, errorStatus, now);
|
||||
@@ -579,7 +583,7 @@ public class DocumentProcessingCoordinator {
|
||||
txOps.updateDocumentRecord(errorRecord);
|
||||
});
|
||||
|
||||
if (limitReached) {
|
||||
if (!retryable) {
|
||||
logger.info("Retry decision for '{}' (fingerprint: {}): FAILED_FINAL — "
|
||||
+ "transient error limit reached ({}/{} attempts). No further retry.",
|
||||
candidate.uniqueIdentifier(), fingerprint.sha256Hex(),
|
||||
@@ -604,9 +608,10 @@ public class DocumentProcessingCoordinator {
|
||||
* following a successful target copy. This is a secondary persistence effort;
|
||||
* its failure is logged but does not change the return value.
|
||||
* <p>
|
||||
* Applies the same transient limit check as {@link #persistTransientError}: if the
|
||||
* incremented counter reaches {@code maxRetriesTransient}, the secondary attempt
|
||||
* is persisted as {@link ProcessingStatus#FAILED_FINAL}.
|
||||
* Applies the same transient limit check as {@link #persistTransientError} via the
|
||||
* central rule in {@link ProcessingOutcomeTransition}: if the incremented counter
|
||||
* reaches {@code maxRetriesTransient}, the secondary attempt is persisted as
|
||||
* {@link ProcessingStatus#FAILED_FINAL}.
|
||||
*/
|
||||
private void persistTransientErrorAfterPersistenceFailure(
|
||||
SourceDocumentCandidate candidate,
|
||||
@@ -617,12 +622,13 @@ public class DocumentProcessingCoordinator {
|
||||
Instant now,
|
||||
String errorMessage) {
|
||||
|
||||
FailureCounters updatedCounters =
|
||||
existingRecord.failureCounters().withIncrementedTransientErrorCount();
|
||||
boolean limitReached = updatedCounters.transientErrorCount() >= maxRetriesTransient;
|
||||
ProcessingStatus errorStatus = limitReached
|
||||
? ProcessingStatus.FAILED_FINAL
|
||||
: ProcessingStatus.FAILED_RETRYABLE;
|
||||
ProcessingOutcomeTransition.ProcessingOutcome transition =
|
||||
ProcessingOutcomeTransition.forKnownDocument(
|
||||
new TechnicalDocumentError(candidate, errorMessage, null),
|
||||
existingRecord.failureCounters(),
|
||||
maxRetriesTransient);
|
||||
FailureCounters updatedCounters = transition.counters();
|
||||
ProcessingStatus errorStatus = transition.overallStatus();
|
||||
|
||||
try {
|
||||
int attemptNumber = processingAttemptRepository.loadNextAttemptNumber(fingerprint);
|
||||
@@ -630,7 +636,7 @@ public class DocumentProcessingCoordinator {
|
||||
fingerprint, context.runId(), attemptNumber, attemptStart, now,
|
||||
errorStatus,
|
||||
errorStatus.name(),
|
||||
errorMessage, !limitReached);
|
||||
errorMessage, transition.retryable());
|
||||
|
||||
DocumentRecord errorRecord = buildTransientErrorRecord(
|
||||
existingRecord, candidate, updatedCounters, errorStatus, now);
|
||||
|
||||
+6
-4
@@ -10,11 +10,13 @@ import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
|
||||
import de.gecheckt.pdf.umbenenner.domain.model.TechnicalDocumentError;
|
||||
|
||||
/**
|
||||
* Pure status and counter transition policy for document processing outcomes.
|
||||
* Authoritative, stateless retry decision rule for all document processing outcomes.
|
||||
* <p>
|
||||
* This class encapsulates the deterministic rules for mapping a pipeline outcome
|
||||
* (pre-check, naming proposal, or failure) to a processing status, updated
|
||||
* failure counters, and retryability flag.
|
||||
* This class is the single production source of truth for mapping a pipeline outcome
|
||||
* (pre-check, naming proposal, or failure) to a processing status, updated failure
|
||||
* counters, and retryability flag. Both the AI pipeline path and the target-copy
|
||||
* finalization path in {@link DocumentProcessingCoordinator} delegate to this class,
|
||||
* so that no duplicate retry logic exists elsewhere.
|
||||
* <p>
|
||||
* The transition logic is independent of persistence, orchestration, or any
|
||||
* infrastructure concern. It is purely declarative and stateless.
|
||||
|
||||
Reference in New Issue
Block a user