Deduplizierung
MQ-Retry erzeugt keine doppelten Dokumente.
Ein Message Driven Bean verarbeitet Rechnungsereignisse nicht idempotent. Nach dem Refactoring gibt es Idempotency-Key, Retry-Klassifikation und Outbox/Inbox-Nachweis.
| Aspekt | Beschreibung |
|---|---|
| Symptom | Bei MQ-Retry wird Versand mehrfach ausgelöst; technische Exceptions und fachliche Ablehnungen landen im gleichen Fehlerpfad. |
| Risiko | Doppelte Briefe, doppelte Buchungen oder unklare DLQ-Klärung. |
| Refactoring-Ziel | Fachlichen Ereignisschlüssel speichern, Verarbeitung klassifizieren und technische Wiederholung von fachlicher Ablehnung trennen. |
@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/InvoiceEvents"),
@ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue")
})
public class LegacyInvoiceEventMdb implements MessageListener {
@EJB private ShippingDocumentService shipping;
@EJB private ArchiveService archive;
public void onMessage(Message message) {
try {
String payload = message.getBody(String.class);
InvoiceCreatedEvent event = InvoiceCreatedEvent.fromJson(payload);
// Problem: bei Rollback/Retry wird Dokument evtl. mehrfach erzeugt.
shipping.createAndSendLetter(event.invoiceNo(), event.customerNo());
archive.storeBusinessDocument(event.invoiceNo(), payload);
} catch (Exception ex) {
throw new EJBException(ex); // alles wird Retry/DLQ, auch fachliche Fehler
}
}
}
CREATE TABLE INBOX_EVENT_PROCESSING (
EVENT_ID VARCHAR2(120) NOT NULL,
CONSUMER_NAME VARCHAR2(80) NOT NULL,
STATUS VARCHAR2(20) NOT NULL,
CORRELATION_ID VARCHAR2(120),
REASON_CODE VARCHAR2(80),
PROCESSED_AT TIMESTAMP,
CREATED_AT TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
CONSTRAINT PK_INBOX_EVENT PRIMARY KEY (EVENT_ID, CONSUMER_NAME)
);
@MessageDriven(activationConfig = {
@ActivationConfigProperty(propertyName = "destinationLookup", propertyValue = "jms/InvoiceEvents"),
@ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue")
})
public class InvoiceEventConsumer implements MessageListener {
@EJB private InboxRepository inboxRepository; // Pattern: Inbox
@EJB private InvoiceEventHandler handler; // Pattern: Application Service
@EJB private EventFailureClassifier failureClassifier; // Pattern: Policy
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void onMessage(Message message) {
String eventId = requiredProperty(message, "eventId");
String correlationId = optionalProperty(message, "correlationId");
String payload = body(message);
if (inboxRepository.alreadyProcessed(eventId, "shipping-documents")) {
return; // Pattern: Idempotent Consumer
}
try {
InvoiceCreatedEvent event = InvoiceCreatedEvent.fromJson(payload);
handler.handle(event, correlationId);
inboxRepository.markProcessed(eventId, "shipping-documents", correlationId);
} catch (BusinessRejectException ex) {
inboxRepository.markRejected(eventId, ex.reasonCode(), correlationId);
// Keine technische Wiederholung: Fachliche Ablehnung wird geklärt.
} catch (Exception ex) {
RetryDecision decision = failureClassifier.classify(ex);
inboxRepository.markTechnicalFailure(eventId, decision.reason(), correlationId);
throw new EJBException(ex); // Retry/DLQ bleibt technisch korrekt.
}
}
}
MQ-Retry erzeugt keine doppelten Dokumente.
Inbox zeigt verarbeitet, fachlich abgelehnt oder technisch fehlgeschlagen.
Nur echte technische Fehler landen im Wiederanlauf.
Correlation-ID und Event-ID sind nachvollziehbar.