优化
This commit is contained in:
+53
-26
@@ -1,5 +1,11 @@
|
||||
package com.baiyue.lastleg.inboundplan.pda.service;
|
||||
|
||||
import com.baiyue.common.core.outbox.EventDeliveryJournal;
|
||||
import com.baiyue.common.core.outbox.EventDeliveryProducer;
|
||||
import com.baiyue.common.core.outbox.EventDeliveryJournal.Outcome;
|
||||
import com.baiyue.common.web.EventDeliveryMetrics;
|
||||
import com.baiyue.common.web.EventDeliveryMetrics.Stream;
|
||||
import com.baiyue.common.web.EventDeliveryObservation;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.web.EventDeliveryFailure;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -22,10 +28,12 @@ import java.util.UUID;
|
||||
/** 至少一次投递海外到国内业务事件;消费服务按 eventId 幂等接收。 */
|
||||
@Component
|
||||
@Slf4j
|
||||
public class InboundUnloadCompletedOutboxRelay {
|
||||
public class InboundUnloadCompletedOutboxRelay implements EventDeliveryProducer {
|
||||
@Resource private UnloadCompletedOutboxRepository repository;
|
||||
@Resource private DirectEventClient client;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private EventDeliveryJournal journal;
|
||||
@Resource private EventDeliveryMetrics metrics;
|
||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||
@Value("${baiyue.event-delivery.lease-seconds:60}") private long leaseSeconds = 60;
|
||||
|
||||
@@ -38,44 +46,63 @@ public class InboundUnloadCompletedOutboxRelay {
|
||||
public void relay() {
|
||||
if (leaseSeconds < 1) throw new IllegalArgumentException("Lease duration must be positive");
|
||||
var policy = new BoundedRetryPolicy(maxAttempts, baseDelaySeconds, maxDelaySeconds, retryJitter);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expiredLeaseBefore = now.minusSeconds(leaseSeconds);
|
||||
for (LastlegEventOutbox candidate : repository.pending(50, now, expiredLeaseBefore)) {
|
||||
String workerId = UUID.randomUUID().toString();
|
||||
now = LocalDateTime.now();
|
||||
expiredLeaseBefore = now.minusSeconds(leaseSeconds);
|
||||
|
||||
if (!repository.claim(candidate.getId(), workerId, now, expiredLeaseBefore)) continue;
|
||||
LastlegEventOutbox row = repository.claimed(candidate.getId(), workerId);
|
||||
LocalDateTime scan = LocalDateTime.now();
|
||||
for (LastlegEventOutbox candidate : repository.pending(50, scan, scan.minusSeconds(leaseSeconds))) {
|
||||
LocalDateTime started = LocalDateTime.now();
|
||||
if ("PROCESSING".equals(candidate.getStatus())) {
|
||||
int failures = (candidate.getRetryCount() == null ? 0 : candidate.getRetryCount()) + 1;
|
||||
LocalDateTime next = policy.exhausted(failures) ? null : started.plus(policy.delayAfterFailure(failures));
|
||||
journal.complete(candidate.getLockedBy(), Outcome.LEASE_EXPIRED, started, "LEASE_EXPIRED", next,
|
||||
() -> repository.expire(candidate.getId(), candidate.getLockedBy(), started.minusSeconds(leaseSeconds),
|
||||
failures, next, "投递租约超时,消费结果未知"));
|
||||
continue; // Recovery always waits for the new backoff window; never sends in this pass.
|
||||
}
|
||||
String worker = UUID.randomUUID().toString();
|
||||
String type = Stream.LASTLEG.type(candidate.getEventType());
|
||||
if (!journal.begin(worker, Stream.LASTLEG.service, type, String.valueOf(candidate.getId()),
|
||||
EventDeliveryObservation.eventId(objectMapper, candidate.getPayloadJson()), Stream.LASTLEG.target(type), started,
|
||||
() -> repository.claim(candidate.getId(), worker, started))) continue;
|
||||
LastlegEventOutbox row = repository.claimed(candidate.getId(), worker);
|
||||
if (row == null) continue;
|
||||
int failures = row.getRetryCount() == null ? 0 : row.getRetryCount();
|
||||
if (policy.exhausted(failures)) {
|
||||
repository.dead(row.getId(), workerId, failures, "自动投递次数已耗尽(含租约超时)");
|
||||
journal.complete(worker, Outcome.DEAD, LocalDateTime.now(), "RETRY_EXHAUSTED", null,
|
||||
() -> repository.dead(row.getId(), worker, failures, "自动投递次数已耗尽(含租约超时)"));
|
||||
continue;
|
||||
}
|
||||
Outcome outcome;
|
||||
String diagnostic = null;
|
||||
String auditError = null;
|
||||
boolean permanent = false;
|
||||
boolean requestStarted = false;
|
||||
try {
|
||||
EventEnvelope event = objectMapper.readValue(row.getPayloadJson(), EventEnvelope.class);
|
||||
EventEnvelopeValidator.requireValid(event);
|
||||
EventEnvelope event = objectMapper.readValue(row.getPayloadJson(), EventEnvelope.class).requireValid();
|
||||
requestStarted = true;
|
||||
var response = client.receive(internalToken, event);
|
||||
requireAcknowledgement(event, response);
|
||||
repository.published(row.getId(), workerId, LocalDateTime.now());
|
||||
EventDeliveryFailure.requireSuccessful(response);
|
||||
response.data().requireMatches(event);
|
||||
outcome = Outcome.valueOf(response.data().outcome().name());
|
||||
} catch (Exception exception) {
|
||||
String error = EventDeliveryFailure.diagnostic(exception, internalToken);
|
||||
boolean permanent = EventDeliveryFailure.isPermanent(exception)
|
||||
permanent = EventDeliveryFailure.isPermanent(exception)
|
||||
|| (!requestStarted && (exception instanceof JsonProcessingException || exception instanceof IllegalArgumentException));
|
||||
if (permanent || policy.exhausted(failures + 1)) repository.dead(row.getId(), workerId, failures + 1, error);
|
||||
else repository.retry(row.getId(), workerId, LocalDateTime.now().plus(policy.delayAfterFailure(failures + 1)),
|
||||
error);
|
||||
log.warn("海外到国内事件投递失败: eventKey={}, reason={}", row.getEventKey(), error);
|
||||
diagnostic = EventDeliveryFailure.diagnostic(exception, internalToken);
|
||||
auditError = EventDeliveryObservation.errorCode(exception, permanent, requestStarted);
|
||||
outcome = permanent || policy.exhausted(failures + 1) ? Outcome.DEAD : Outcome.RETRY;
|
||||
}
|
||||
// Persistence failures propagate. Do not reinterpret a failed commit as an HTTP failure.
|
||||
LocalDateTime finished = LocalDateTime.now();
|
||||
LocalDateTime next = outcome == Outcome.RETRY ? finished.plus(policy.delayAfterFailure(failures + 1)) : null;
|
||||
Outcome result = outcome;
|
||||
String error = diagnostic;
|
||||
int retries = failures + 1;
|
||||
journal.complete(worker, result, finished, auditError, next, () -> {
|
||||
if (result == Outcome.DEAD) return repository.dead(row.getId(), worker, retries, error);
|
||||
if (result == Outcome.RETRY) return repository.retry(row.getId(), worker, next, error);
|
||||
return repository.published(row.getId(), worker, finished);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void requireAcknowledgement(EventEnvelope event,
|
||||
com.baiyue.common.core.api.ApiResponse<DeliveryAck> response) {
|
||||
EventDeliveryFailure.requireSuccessful(response);
|
||||
response.data().requireMatches(event);
|
||||
}
|
||||
@Scheduled(fixedDelayString = "${baiyue.event-delivery.metrics-sample-ms:30000}")
|
||||
public void sampleMetrics() { metrics.sample(Stream.LASTLEG, LocalDateTime.now()); }
|
||||
}
|
||||
|
||||
+1
-2
@@ -7,7 +7,6 @@ import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.common.event.OverseasContainerUnloadedV1;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.lastleg.entity.LastlegEventOutbox;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
@@ -78,7 +77,7 @@ public class InboundUnloadCompletedOutboxService {
|
||||
JsonNode payloadNode = objectMapper.valueToTree(payload);
|
||||
String traceId = EventIdentifiers.newUuid();
|
||||
EventEnvelope envelope = new EventEnvelope(EventIdentifiers.newUuid(), EventTypes.OVERSEAS_CONTAINER_UNLOADED,
|
||||
1, Instant.now(), RegionCode.OVERSEAS, RegionCode.CN, "0", "CONTAINER_BOOKING",
|
||||
1, Instant.now(), "0", "CONTAINER_BOOKING",
|
||||
sourceContainerBookingId, Math.max(1, aggregateVersion), 1L, traceId, eventKey, null, payloadNode,
|
||||
EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payloadNode))).requireValid();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user