This commit is contained in:
2026-09-29 17:55:54 +08:00
parent 41d8387cd5
commit 54ad26c607
111 changed files with 1837 additions and 962 deletions
@@ -27,8 +27,11 @@ public class OverseasFeeOutboxRepository {
public boolean claim(long id, String worker, LocalDateTime now, LocalDateTime stale) {
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").or(processing ->
.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;
}
@@ -40,6 +43,12 @@ public class OverseasFeeOutboxRepository {
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")
.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)
.set(LastlegEventOutbox::getNextRetryAt, next).set(LastlegEventOutbox::getLastError, truncate(error)));
@@ -1,7 +1,8 @@
package com.baiyue.lastleg.financefee.service;
import com.baiyue.common.event.EventEnvelope;
import com.baiyue.lastleg.client.EventBridgeClient;
import com.baiyue.common.core.retry.BoundedRetryPolicy;
import com.baiyue.lastleg.client.DirectEventClient;
import com.baiyue.lastleg.entity.LastlegEventOutbox;
import com.baiyue.lastleg.financefee.repository.OverseasFeeOutboxRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -10,7 +11,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.lang.management.ManagementFactory;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.UUID;
@@ -18,28 +18,45 @@ import java.util.UUID;
@Component
public class OverseasFeeOutboxRelay {
@Resource private OverseasFeeOutboxRepository repository;
@Resource private EventBridgeClient client;
@Resource private DirectEventClient client;
@Resource private ObjectMapper objectMapper;
@Value("${baiyue.security.internal-service-token:}") private String token;
private final String worker = ManagementFactory.getRuntimeMXBean().getName() + ':' + UUID.randomUUID();
@Value("${baiyue.event-delivery.max-attempts:12}") private int maxAttempts = 12;
@Value("${baiyue.event-delivery.base-delay-seconds:5}") private long baseDelaySeconds = 5;
@Value("${baiyue.event-delivery.max-delay-seconds:1800}") private long maxDelaySeconds = 1800;
@Value("${baiyue.event-delivery.jitter:0.1}") private double retryJitter = 0.1;
@Value("${baiyue.event-delivery.lease-seconds:60}") private long leaseSeconds = 60;
@Scheduled(fixedDelayString = "${baiyue.finance-fee-relay.fixed-delay:3000}")
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(30))) {
if (!repository.claim(candidate.getId(), worker, now, now.minusSeconds(30))) continue;
for (LastlegEventOutbox candidate : repository.pending(50, now, now.minusSeconds(leaseSeconds))) {
String worker = UUID.randomUUID().toString();
now = LocalDateTime.now(ZoneOffset.UTC);
if (!repository.claim(candidate.getId(), worker, now, now.minusSeconds(leaseSeconds))) 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, "自动投递次数已耗尽(含租约超时)");
continue;
}
try {
EventEnvelope envelope = objectMapper.readValue(row.getPayloadJson(), EventEnvelope.class).requireValid();
var response = client.receive(token, envelope);
if (response == null || !"200".equals(response.code()) || response.data() == null
|| !envelope.eventId().equals(response.data().eventId()))
throw new IllegalStateException("Event Bridge未确认海外费用事件");
if (response == null || !response.successful() || response.data() == null)
throw new IllegalStateException("Finance未确认费用事件");
response.data().requireMatches(envelope);
repository.published(row.getId(), worker, LocalDateTime.now(ZoneOffset.UTC));
} catch (Exception exception) {
int retries = row.getRetryCount() == null ? 1 : row.getRetryCount() + 1;
repository.retry(row.getId(), worker, retries, now.plusSeconds(Math.min(900, 5L << Math.min(retries, 7))), exception.getMessage());
int retries = failures + 1;
if (policy.exhausted(retries)) repository.dead(row.getId(), worker, retries, exception.getMessage());
else repository.retry(row.getId(), worker, retries, LocalDateTime.now(ZoneOffset.UTC).plus(policy.delayAfterFailure(retries)), exception.getMessage());
}
}
}