优化
This commit is contained in:
+22
-14
@@ -24,33 +24,41 @@ public class OverseasFeeOutboxRepository {
|
||||
.or(processing -> processing.eq(LastlegEventOutbox::getStatus, "PROCESSING").le(LastlegEventOutbox::getLockedAt, stale)))
|
||||
.orderByAsc(LastlegEventOutbox::getCreatedAt).last("LIMIT " + limit));
|
||||
}
|
||||
public boolean claim(long id, String worker, LocalDateTime now, LocalDateTime stale) {
|
||||
public boolean claim(long id, String worker, LocalDateTime now) {
|
||||
return mapper.update(null, new LambdaUpdateWrapper<LastlegEventOutbox>()
|
||||
.eq(LastlegEventOutbox::getId, id).eq(LastlegEventOutbox::getEventType, EventTypes.BUSINESS_FEE_SOURCE_CHANGED)
|
||||
.and(status -> status.eq(LastlegEventOutbox::getStatus, "PENDING")
|
||||
.and(retry -> retry.isNull(LastlegEventOutbox::getNextRetryAt).or().le(LastlegEventOutbox::getNextRetryAt, now))
|
||||
.or(processing ->
|
||||
processing.eq(LastlegEventOutbox::getStatus, "PROCESSING").le(LastlegEventOutbox::getLockedAt, stale)))
|
||||
.setSql("retry_count = retry_count + CASE WHEN status='PROCESSING' THEN 1 ELSE 0 END")
|
||||
.set(LastlegEventOutbox::getStatus, "PROCESSING").set(LastlegEventOutbox::getLockedBy, worker)
|
||||
.set(LastlegEventOutbox::getLockedAt, now)) == 1;
|
||||
.eq(LastlegEventOutbox::getStatus, "PENDING")
|
||||
.and(retry -> retry.isNull(LastlegEventOutbox::getNextRetryAt).or().le(LastlegEventOutbox::getNextRetryAt, now))
|
||||
.set(LastlegEventOutbox::getStatus, "PROCESSING")
|
||||
.set(LastlegEventOutbox::getLockedBy, worker).set(LastlegEventOutbox::getLockedAt, now)) == 1;
|
||||
}
|
||||
|
||||
public int expire(long id, String worker, LocalDateTime cutoff, int failures, LocalDateTime next, String error) {
|
||||
return mapper.update(null, new LambdaUpdateWrapper<LastlegEventOutbox>()
|
||||
.eq(LastlegEventOutbox::getId, id).eq(LastlegEventOutbox::getEventType, EventTypes.BUSINESS_FEE_SOURCE_CHANGED)
|
||||
.eq(LastlegEventOutbox::getStatus, "PROCESSING").eq(LastlegEventOutbox::getLockedBy, worker)
|
||||
.le(LastlegEventOutbox::getLockedAt, cutoff)
|
||||
.set(LastlegEventOutbox::getStatus, next == null ? "DEAD" : "PENDING")
|
||||
.set(LastlegEventOutbox::getRetryCount, failures).set(LastlegEventOutbox::getNextRetryAt, next)
|
||||
.set(LastlegEventOutbox::getLockedBy, null).set(LastlegEventOutbox::getLockedAt, null)
|
||||
.set(LastlegEventOutbox::getLastError, truncate(error)));
|
||||
}
|
||||
public LastlegEventOutbox claimed(long id, String worker) {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<LastlegEventOutbox>().eq(LastlegEventOutbox::getId, id)
|
||||
.eq(LastlegEventOutbox::getStatus, "PROCESSING").eq(LastlegEventOutbox::getLockedBy, worker));
|
||||
}
|
||||
public void published(long id, String worker, LocalDateTime now) {
|
||||
mapper.update(null, claimed(id, worker, "PUBLISHED").set(LastlegEventOutbox::getPublishedAt, now)
|
||||
public int published(long id, String worker, LocalDateTime now) {
|
||||
return mapper.update(null, claimed(id, worker, "PUBLISHED").set(LastlegEventOutbox::getPublishedAt, now)
|
||||
.set(LastlegEventOutbox::getNextRetryAt, null).set(LastlegEventOutbox::getLastError, null));
|
||||
}
|
||||
public void dead(long id, String worker, int failures, String error) {
|
||||
mapper.update(null, claimed(id, worker, "DEAD")
|
||||
public int dead(long id, String worker, int failures, String error) {
|
||||
return mapper.update(null, claimed(id, worker, "DEAD")
|
||||
.set(LastlegEventOutbox::getRetryCount, failures)
|
||||
.set(LastlegEventOutbox::getNextRetryAt, null)
|
||||
.set(LastlegEventOutbox::getLastError, truncate(error)));
|
||||
}
|
||||
public void retry(long id, String worker, int retryCount, LocalDateTime next, String error) {
|
||||
mapper.update(null, claimed(id, worker, "PENDING").set(LastlegEventOutbox::getRetryCount, retryCount)
|
||||
public int retry(long id, String worker, int retryCount, LocalDateTime next, String error) {
|
||||
return mapper.update(null, claimed(id, worker, "PENDING").set(LastlegEventOutbox::getRetryCount, retryCount)
|
||||
.set(LastlegEventOutbox::getNextRetryAt, next).set(LastlegEventOutbox::getLastError, truncate(error)));
|
||||
}
|
||||
private LambdaUpdateWrapper<LastlegEventOutbox> claimed(long id, String worker, String status) {
|
||||
|
||||
+52
-16
@@ -1,5 +1,11 @@
|
||||
package com.baiyue.lastleg.financefee.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;
|
||||
@@ -18,10 +24,12 @@ import java.time.ZoneOffset;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
public class OverseasFeeOutboxRelay {
|
||||
public class OverseasFeeOutboxRelay implements EventDeliveryProducer {
|
||||
@Resource private OverseasFeeOutboxRepository 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 token;
|
||||
|
||||
@Value("${baiyue.event-delivery.max-attempts:12}") private int maxAttempts = 12;
|
||||
@@ -35,35 +43,63 @@ public class OverseasFeeOutboxRelay {
|
||||
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(ZoneOffset.UTC);
|
||||
for (LastlegEventOutbox candidate : repository.pending(50, now, now.minusSeconds(leaseSeconds))) {
|
||||
LocalDateTime scan = LocalDateTime.now(ZoneOffset.UTC);
|
||||
for (LastlegEventOutbox candidate : repository.pending(50, scan, scan.minusSeconds(leaseSeconds))) {
|
||||
LocalDateTime started = LocalDateTime.now(ZoneOffset.UTC);
|
||||
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();
|
||||
now = LocalDateTime.now(ZoneOffset.UTC);
|
||||
|
||||
if (!repository.claim(candidate.getId(), worker, now, now.minusSeconds(leaseSeconds))) continue;
|
||||
String type = Stream.FEE.type(candidate.getEventType());
|
||||
if (!journal.begin(worker, Stream.FEE.service, type, String.valueOf(candidate.getId()),
|
||||
EventDeliveryObservation.eventId(objectMapper, candidate.getPayloadJson()), Stream.FEE.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(), worker, failures, "自动投递次数已耗尽(含租约超时)");
|
||||
journal.complete(worker, Outcome.DEAD, LocalDateTime.now(ZoneOffset.UTC), "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 envelope = objectMapper.readValue(row.getPayloadJson(), EventEnvelope.class).requireValid();
|
||||
EventEnvelope event = objectMapper.readValue(row.getPayloadJson(), EventEnvelope.class).requireValid();
|
||||
requestStarted = true;
|
||||
var response = client.receive(token, envelope);
|
||||
var response = client.receive(token, event);
|
||||
EventDeliveryFailure.requireSuccessful(response);
|
||||
response.data().requireMatches(envelope);
|
||||
repository.published(row.getId(), worker, LocalDateTime.now(ZoneOffset.UTC));
|
||||
response.data().requireMatches(event);
|
||||
outcome = Outcome.valueOf(response.data().outcome().name());
|
||||
} catch (Exception exception) {
|
||||
String error = EventDeliveryFailure.diagnostic(exception, token);
|
||||
boolean permanent = EventDeliveryFailure.isPermanent(exception)
|
||||
permanent = EventDeliveryFailure.isPermanent(exception)
|
||||
|| (!requestStarted && (exception instanceof JsonProcessingException || exception instanceof IllegalArgumentException));
|
||||
int retries = failures + 1;
|
||||
if (permanent || policy.exhausted(retries)) repository.dead(row.getId(), worker, retries, error);
|
||||
else repository.retry(row.getId(), worker, retries, LocalDateTime.now(ZoneOffset.UTC).plus(policy.delayAfterFailure(retries)), error);
|
||||
diagnostic = EventDeliveryFailure.diagnostic(exception, token);
|
||||
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(ZoneOffset.UTC);
|
||||
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, retries, next, error);
|
||||
return repository.published(row.getId(), worker, finished);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${baiyue.event-delivery.metrics-sample-ms:30000}")
|
||||
public void sampleMetrics() { metrics.sample(Stream.FEE, LocalDateTime.now(ZoneOffset.UTC)); }
|
||||
}
|
||||
|
||||
+1
-2
@@ -7,7 +7,6 @@ import com.baiyue.common.event.EventIdentifiers;
|
||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.lastleg.entity.LastlegEventOutbox;
|
||||
import com.baiyue.lastleg.financefee.entity.OverseasFeeSource;
|
||||
import com.baiyue.lastleg.mapper.LastlegEventOutboxMapper;
|
||||
@@ -37,7 +36,7 @@ public class OverseasFeeOutboxService {
|
||||
+ source.getSourceType() + ':' + source.getSourceFeeId() + ':' + source.getSourceVersion() + ':' + action;
|
||||
if (mapper.findByEventKey(eventKey) != null) return;
|
||||
EventEnvelope envelope = new EventEnvelope(EventIdentifiers.newUuid(),
|
||||
EventTypes.BUSINESS_FEE_SOURCE_CHANGED, 1, Instant.now(), RegionCode.OVERSEAS, RegionCode.CN,
|
||||
EventTypes.BUSINESS_FEE_SOURCE_CHANGED, 1, Instant.now(),
|
||||
source.getCustomerId(), source.getSourceType(), String.valueOf(source.getSourceFeeId()),
|
||||
source.getSourceVersion(), null, EventIdentifiers.newUuid(), eventKey, null, payloadNode,
|
||||
EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payloadNode))).requireValid();
|
||||
|
||||
Reference in New Issue
Block a user