优化
This commit is contained in:
+18
@@ -2,6 +2,7 @@ package com.baiyue.eventbridge.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.eventbridge.domain.BridgeInbox;
|
||||
import com.baiyue.eventbridge.metrics.InboxMetricsSnapshot;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
@@ -60,4 +61,21 @@ public interface BridgeInboxMapper extends BaseMapper<BridgeInbox> {
|
||||
WHERE event_id=#{eventId} AND status='DEAD'
|
||||
""")
|
||||
int replayDead(@Param("eventId") String eventId, @Param("reason") String reason);
|
||||
|
||||
/**
|
||||
* Scheduled observability aggregation. The status predicate uses idx_bridge_inbox_consume;
|
||||
* Prometheus collection never invokes this query directly.
|
||||
*/
|
||||
@Select("""
|
||||
SELECT target_region AS targetRegion, event_type AS eventType,
|
||||
SUM(CASE WHEN status IN ('RECEIVED','RETRY','PROCESSING') THEN 1 ELSE 0 END) AS backlogCount,
|
||||
COALESCE(TIMESTAMPDIFF(MICROSECOND,
|
||||
MIN(CASE WHEN status IN ('RECEIVED','RETRY','PROCESSING') THEN received_at END),
|
||||
CURRENT_TIMESTAMP(3)) / 1000000.0, 0) AS oldestPendingAgeSeconds,
|
||||
SUM(CASE WHEN status='DEAD' THEN 1 ELSE 0 END) AS deadCount
|
||||
FROM bridge_inbox
|
||||
WHERE status IN ('RECEIVED','RETRY','PROCESSING','DEAD')
|
||||
GROUP BY target_region, event_type
|
||||
""")
|
||||
List<InboxMetricsSnapshot> observabilitySnapshots();
|
||||
}
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package com.baiyue.eventbridge.metrics;
|
||||
|
||||
import com.baiyue.eventbridge.mapper.BridgeInboxMapper;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.MultiGauge;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Periodically samples indexed Inbox aggregates; Prometheus scrapes only the cached gauges. */
|
||||
@Component
|
||||
@Slf4j
|
||||
public class EventBridgeInboxMetricsSampler {
|
||||
public static final String BACKLOG_METRIC = "baiyue.event.bridge.inbox.backlog";
|
||||
public static final String OLDEST_AGE_METRIC = "baiyue.event.bridge.inbox.oldest.age.seconds";
|
||||
public static final String DEAD_METRIC = "baiyue.event.bridge.inbox.dead";
|
||||
|
||||
private final BridgeInboxMapper inboxMapper;
|
||||
private final MultiGauge backlog;
|
||||
private final MultiGauge oldestAge;
|
||||
private final MultiGauge dead;
|
||||
|
||||
public EventBridgeInboxMetricsSampler(BridgeInboxMapper inboxMapper, MeterRegistry registry) {
|
||||
this.inboxMapper = inboxMapper;
|
||||
this.backlog = MultiGauge.builder(BACKLOG_METRIC)
|
||||
.description("Events waiting for downstream acknowledgement").register(registry);
|
||||
this.oldestAge = MultiGauge.builder(OLDEST_AGE_METRIC)
|
||||
.description("Age in seconds of the oldest pending Inbox event").register(registry);
|
||||
this.dead = MultiGauge.builder(DEAD_METRIC)
|
||||
.description("Current number of DEAD Inbox events").register(registry);
|
||||
}
|
||||
|
||||
@Scheduled(initialDelayString = "${baiyue.event-bridge.metrics.initial-delay:5000}",
|
||||
fixedDelayString = "${baiyue.event-bridge.metrics.sample-delay:30000}")
|
||||
public void sample() {
|
||||
final List<InboxMetricsSnapshot> snapshots;
|
||||
try {
|
||||
snapshots = inboxMapper.observabilitySnapshots();
|
||||
} catch (RuntimeException exception) {
|
||||
log.warn("Event Bridge Inbox 指标采样失败,保留上次成功快照", exception);
|
||||
return;
|
||||
}
|
||||
Map<MetricKey, Aggregate> aggregates = aggregate(snapshots);
|
||||
backlog.register(rows(aggregates, MetricValue.BACKLOG), true);
|
||||
oldestAge.register(rows(aggregates, MetricValue.OLDEST_AGE), true);
|
||||
dead.register(rows(aggregates, MetricValue.DEAD), true);
|
||||
}
|
||||
|
||||
private Map<MetricKey, Aggregate> aggregate(List<InboxMetricsSnapshot> snapshots) {
|
||||
Map<MetricKey, Aggregate> result = new LinkedHashMap<>();
|
||||
if (snapshots == null) {
|
||||
return result;
|
||||
}
|
||||
for (InboxMetricsSnapshot snapshot : snapshots) {
|
||||
MetricKey key = new MetricKey(EventBridgeMetrics.targetRegion(snapshot.getTargetRegion()),
|
||||
EventBridgeMetrics.eventType(snapshot.getEventType()));
|
||||
result.computeIfAbsent(key, ignored -> new Aggregate()).add(snapshot);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<MultiGauge.Row<?>> rows(Map<MetricKey, Aggregate> aggregates, MetricValue value) {
|
||||
List<MultiGauge.Row<?>> rows = new ArrayList<>();
|
||||
aggregates.forEach((key, aggregate) -> rows.add(MultiGauge.Row.of(
|
||||
Tags.of("target_region", key.targetRegion, "event_type", key.eventType),
|
||||
value.value(aggregate))));
|
||||
return rows;
|
||||
}
|
||||
|
||||
private record MetricKey(String targetRegion, String eventType) { }
|
||||
|
||||
private static final class Aggregate {
|
||||
private long backlog;
|
||||
private double oldestAge;
|
||||
private long dead;
|
||||
|
||||
void add(InboxMetricsSnapshot snapshot) {
|
||||
backlog += snapshot.getBacklogCount();
|
||||
oldestAge = Math.max(oldestAge, snapshot.getOldestPendingAgeSeconds());
|
||||
dead += snapshot.getDeadCount();
|
||||
}
|
||||
}
|
||||
|
||||
private enum MetricValue {
|
||||
BACKLOG {
|
||||
@Override Number value(Aggregate aggregate) { return aggregate.backlog; }
|
||||
},
|
||||
OLDEST_AGE {
|
||||
@Override Number value(Aggregate aggregate) { return aggregate.oldestAge; }
|
||||
},
|
||||
DEAD {
|
||||
@Override Number value(Aggregate aggregate) { return aggregate.dead; }
|
||||
};
|
||||
|
||||
abstract Number value(Aggregate aggregate);
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package com.baiyue.eventbridge.metrics;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.eventbridge.domain.BridgeInbox;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
/** Low-cardinality transport metrics for the cross-region event bridge. */
|
||||
@Component
|
||||
@Slf4j
|
||||
public class EventBridgeMetrics {
|
||||
public static final String RECEIVE_METRIC = "baiyue.event.bridge.receive";
|
||||
public static final String TRANSPORT_METRIC = "baiyue.event.bridge.transport";
|
||||
public static final String DELIVERY_LATENCY_METRIC = "baiyue.event.bridge.delivery.latency";
|
||||
|
||||
private static final Set<String> KNOWN_EVENT_TYPES = Set.of(
|
||||
EventTypes.ORDER_APPROVED,
|
||||
EventTypes.ORDER_CANCELLED,
|
||||
EventTypes.FIRST_LEG_SHIPMENT_DEPARTED,
|
||||
EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,
|
||||
EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED,
|
||||
EventTypes.OVERSEAS_CONTAINER_UNLOADED,
|
||||
EventTypes.IAM_USER_STATUS_CHANGED,
|
||||
EventTypes.IAM_TENANT_STATUS_CHANGED,
|
||||
EventTypes.IAM_PERMISSION_VERSION_CHANGED,
|
||||
EventTypes.LAST_MILE_HANDOFF_ACCEPTED,
|
||||
EventTypes.LAST_MILE_HANDOFF_REJECTED,
|
||||
EventTypes.LAST_MILE_MILESTONE_CHANGED,
|
||||
EventTypes.LAST_MILE_COMPLETED,
|
||||
EventTypes.TRACKING_MILESTONE_PROJECTED);
|
||||
|
||||
private final MeterRegistry registry;
|
||||
private final Clock clock;
|
||||
|
||||
public EventBridgeMetrics(MeterRegistry registry) {
|
||||
this(registry, Clock.systemDefaultZone());
|
||||
}
|
||||
|
||||
EventBridgeMetrics(MeterRegistry registry, Clock clock) {
|
||||
this.registry = registry;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public void recordReceive(EventEnvelope envelope, ReceiveResult result) {
|
||||
try {
|
||||
registry.counter(RECEIVE_METRIC,
|
||||
"target_region", targetRegion(envelope.targetRegion().name()),
|
||||
"event_type", eventType(envelope.eventType()),
|
||||
"result", result.name()).increment();
|
||||
} catch (RuntimeException exception) {
|
||||
log.warn("Event Bridge 接收指标记录失败,不影响事件事务", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public void recordTransport(BridgeInbox event, TransportResult result) {
|
||||
try {
|
||||
String targetRegion = targetRegion(event.getTargetRegion());
|
||||
String eventType = eventType(event.getEventType());
|
||||
registry.counter(TRANSPORT_METRIC,
|
||||
"target_region", targetRegion,
|
||||
"event_type", eventType,
|
||||
"result", result.name()).increment();
|
||||
if (result.acknowledged() && event.getReceivedAt() != null) {
|
||||
Duration latency = Duration.between(event.getReceivedAt(), LocalDateTime.now(clock));
|
||||
if (latency.isNegative()) {
|
||||
latency = Duration.ZERO;
|
||||
}
|
||||
Timer.builder(DELIVERY_LATENCY_METRIC)
|
||||
.description("Time from Event Bridge receipt to downstream acknowledgement")
|
||||
.serviceLevelObjectives(Duration.ofSeconds(60), Duration.ofMinutes(15))
|
||||
.tags("target_region", targetRegion, "event_type", eventType)
|
||||
.register(registry).record(latency);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
log.warn("Event Bridge 传输指标记录失败,不影响可靠性状态机", exception);
|
||||
}
|
||||
}
|
||||
|
||||
public static String targetRegion(String value) {
|
||||
String normalized = value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
|
||||
return "CN".equals(normalized) || "OVERSEAS".equals(normalized) ? normalized : "OTHER";
|
||||
}
|
||||
|
||||
public static String eventType(String value) {
|
||||
return value != null && KNOWN_EVENT_TYPES.contains(value) ? value : "OTHER";
|
||||
}
|
||||
|
||||
public enum ReceiveResult {
|
||||
RECEIVED,
|
||||
DUPLICATE,
|
||||
CONFLICT
|
||||
}
|
||||
|
||||
public enum TransportResult {
|
||||
DELIVERED(true),
|
||||
RETRY(false),
|
||||
DEAD(false),
|
||||
ACKNOWLEDGED_LEASE_LOST(true),
|
||||
FAILED_LEASE_LOST(false),
|
||||
MANUAL_REPLAY(false);
|
||||
|
||||
private final boolean acknowledged;
|
||||
|
||||
TransportResult(boolean acknowledged) {
|
||||
this.acknowledged = acknowledged;
|
||||
}
|
||||
|
||||
boolean acknowledged() {
|
||||
return acknowledged;
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.baiyue.eventbridge.metrics;
|
||||
|
||||
/** Result row of the bounded, scheduled Inbox observability aggregation. */
|
||||
public class InboxMetricsSnapshot {
|
||||
private String targetRegion;
|
||||
private String eventType;
|
||||
private long backlogCount;
|
||||
private double oldestPendingAgeSeconds;
|
||||
private long deadCount;
|
||||
|
||||
public String getTargetRegion() {
|
||||
return targetRegion;
|
||||
}
|
||||
|
||||
public void setTargetRegion(String targetRegion) {
|
||||
this.targetRegion = targetRegion;
|
||||
}
|
||||
|
||||
public String getEventType() {
|
||||
return eventType;
|
||||
}
|
||||
|
||||
public void setEventType(String eventType) {
|
||||
this.eventType = eventType;
|
||||
}
|
||||
|
||||
public long getBacklogCount() {
|
||||
return backlogCount;
|
||||
}
|
||||
|
||||
public void setBacklogCount(long backlogCount) {
|
||||
this.backlogCount = backlogCount;
|
||||
}
|
||||
|
||||
public double getOldestPendingAgeSeconds() {
|
||||
return oldestPendingAgeSeconds;
|
||||
}
|
||||
|
||||
public void setOldestPendingAgeSeconds(double oldestPendingAgeSeconds) {
|
||||
this.oldestPendingAgeSeconds = oldestPendingAgeSeconds;
|
||||
}
|
||||
|
||||
public long getDeadCount() {
|
||||
return deadCount;
|
||||
}
|
||||
|
||||
public void setDeadCount(long deadCount) {
|
||||
this.deadCount = deadCount;
|
||||
}
|
||||
}
|
||||
+5
@@ -5,6 +5,7 @@ import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
import com.baiyue.eventbridge.domain.BridgeInbox;
|
||||
import com.baiyue.eventbridge.dto.EventReceiveResult;
|
||||
import com.baiyue.eventbridge.metrics.EventBridgeMetrics;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -23,6 +24,7 @@ import java.util.Objects;
|
||||
public class EventInboxService {
|
||||
@Resource private InboxStore inboxStore;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private EventBridgeMetrics metrics;
|
||||
|
||||
@Value("${baiyue.region:CN}")
|
||||
private String localRegion;
|
||||
@@ -44,6 +46,7 @@ public class EventInboxService {
|
||||
BridgeInbox inbox = toInbox(envelope);
|
||||
try {
|
||||
inboxStore.insert(inbox);
|
||||
metrics.recordReceive(envelope, EventBridgeMetrics.ReceiveResult.RECEIVED);
|
||||
return new EventReceiveResult(envelope.eventId(), "已接收", inbox.getReceivedAt());
|
||||
} catch (DuplicateKeyException exception) {
|
||||
BridgeInbox concurrent = inboxStore.findByEventId(envelope.eventId());
|
||||
@@ -56,8 +59,10 @@ public class EventInboxService {
|
||||
|
||||
private EventReceiveResult duplicateOrConflict(BridgeInbox existing, EventEnvelope envelope) {
|
||||
if (!sameEnvelope(existing, envelope)) {
|
||||
metrics.recordReceive(envelope, EventBridgeMetrics.ReceiveResult.CONFLICT);
|
||||
throw new IllegalArgumentException("相同 eventId 对应的事件信封或 payloadHash 不一致,拒绝覆盖既有事件");
|
||||
}
|
||||
metrics.recordReceive(envelope, EventBridgeMetrics.ReceiveResult.DUPLICATE);
|
||||
return new EventReceiveResult(envelope.eventId(), "重复", existing.getReceivedAt());
|
||||
}
|
||||
|
||||
|
||||
+26
@@ -1,13 +1,23 @@
|
||||
package com.baiyue.eventbridge.service;
|
||||
|
||||
import com.baiyue.eventbridge.domain.BridgeDeliveryAttempt;
|
||||
import com.baiyue.eventbridge.domain.BridgeInbox;
|
||||
import com.baiyue.eventbridge.mapper.BridgeDeliveryAttemptMapper;
|
||||
import com.baiyue.eventbridge.mapper.BridgeInboxMapper;
|
||||
import com.baiyue.eventbridge.metrics.EventBridgeMetrics;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class EventReplayService {
|
||||
@Resource private BridgeInboxMapper inboxMapper;
|
||||
@Resource private BridgeDeliveryAttemptMapper attemptMapper;
|
||||
@Resource private EventBridgeMetrics metrics;
|
||||
|
||||
@Transactional
|
||||
public void replayDead(String eventId, String operatorId, String reason) {
|
||||
if (operatorId == null || operatorId.isBlank()) {
|
||||
throw new IllegalArgumentException("人工补投必须提供操作人");
|
||||
@@ -19,6 +29,22 @@ public class EventReplayService {
|
||||
if (inboxMapper.replayDead(eventId, auditReason) != 1) {
|
||||
throw new IllegalStateException("事件不存在或未处于 DEAD 状态");
|
||||
}
|
||||
BridgeInbox event = inboxMapper.selectById(eventId);
|
||||
if (event == null) {
|
||||
throw new IllegalStateException("事件补投状态已更新但事件记录不存在");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
BridgeDeliveryAttempt audit = new BridgeDeliveryAttempt();
|
||||
audit.setEventId(eventId);
|
||||
audit.setSourceRegion(event.getSourceRegion());
|
||||
audit.setTargetRegion(event.getTargetRegion());
|
||||
audit.setAttemptNo(inboxMapper.nextAttemptNo(eventId));
|
||||
audit.setStatus("MANUAL_REPLAY");
|
||||
audit.setErrorMessage(auditReason);
|
||||
audit.setStartedAt(now);
|
||||
audit.setFinishedAt(now);
|
||||
attemptMapper.insert(audit);
|
||||
metrics.recordTransport(event, EventBridgeMetrics.TransportResult.MANUAL_REPLAY);
|
||||
}
|
||||
|
||||
private static String abbreviate(String value, int maxLength) {
|
||||
|
||||
+22
-2
@@ -42,11 +42,31 @@ public class HttpFirstlegEventClient implements FirstlegEventClient {
|
||||
.header("X-Internal-Service-Token", serviceToken)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(envelope))).build();
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
JsonNode responseBody = objectMapper.readTree(response.body());
|
||||
JsonNode responseBody;
|
||||
try {
|
||||
responseBody = objectMapper.readTree(response.body());
|
||||
} catch (Exception ignored) {
|
||||
responseBody = null;
|
||||
}
|
||||
if (response.statusCode() / 100 != 2 || responseBody == null
|
||||
|| !"200".equals(responseBody.path("code").asText())) {
|
||||
throw new IllegalStateException("头程消费失败: HTTP " + response.statusCode());
|
||||
throw new IllegalStateException(failureMessage(response.statusCode(), responseBody));
|
||||
}
|
||||
return String.valueOf(response.statusCode());
|
||||
}
|
||||
|
||||
static String failureMessage(int statusCode, JsonNode body) {
|
||||
String code = body == null ? "missing" : body.path("code").asText("missing");
|
||||
String message = body == null ? "non-json response" : body.path("message").asText("missing");
|
||||
String requestId = body == null ? "missing" : body.path("requestId").asText("missing");
|
||||
String traceId = body == null ? "missing" : body.path("traceId").asText("missing");
|
||||
return "头程消费失败: HTTP " + statusCode + ", code=" + bounded(code)
|
||||
+ ", message=" + bounded(message) + ", requestId=" + bounded(requestId)
|
||||
+ ", traceId=" + bounded(traceId);
|
||||
}
|
||||
|
||||
private static String bounded(String value) {
|
||||
String safe = value == null || value.isBlank() ? "missing" : value.replaceAll("[\\r\\n]+", " ");
|
||||
return safe.length() <= 240 ? safe : safe.substring(0, 240);
|
||||
}
|
||||
}
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package com.baiyue.eventbridge.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.eventbridge.config.EventDispatcherProperties;
|
||||
import com.baiyue.eventbridge.domain.BridgeInbox;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
|
||||
@Component
|
||||
public class HttpSecurityProjectionEventClient implements SecurityProjectionEventClient {
|
||||
private final ObjectMapper objectMapper;
|
||||
private final EventDispatcherProperties properties;
|
||||
@Value("${baiyue.event-dispatcher.security-consumer-url}") private URI consumerUrl;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String serviceToken;
|
||||
private HttpClient httpClient;
|
||||
|
||||
public HttpSecurityProjectionEventClient(ObjectMapper objectMapper, EventDispatcherProperties properties) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@PostConstruct void initialize() {
|
||||
httpClient = HttpClient.newBuilder().connectTimeout(properties.connectTimeout()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String deliver(BridgeInbox row) throws Exception {
|
||||
EventEnvelope envelope = new EventEnvelope(row.getEventId(), row.getEventType(), row.getEventVersion(),
|
||||
row.getOccurredAt(), RegionCode.valueOf(row.getSourceRegion()), RegionCode.valueOf(row.getTargetRegion()),
|
||||
row.getCustomerId(), row.getAggregateType(), row.getAggregateId(), row.getAggregateVersion(),
|
||||
row.getHandoverVersion(), row.getTraceId(), row.getCorrelationId(), row.getCausationId(),
|
||||
objectMapper.readTree(row.getPayloadJson()), row.getPayloadHash());
|
||||
HttpRequest request = HttpRequest.newBuilder(consumerUrl).timeout(properties.requestTimeout())
|
||||
.header("Content-Type", "application/json")
|
||||
.header("X-Internal-Service-Token", serviceToken)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(envelope))).build();
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
JsonNode body;
|
||||
try { body = objectMapper.readTree(response.body()); }
|
||||
catch (Exception exception) { body = null; }
|
||||
if (response.statusCode() / 100 != 2 || body == null || !"200".equals(body.path("code").asText())
|
||||
|| !row.getEventId().equals(body.path("data").path("eventId").asText())) {
|
||||
throw new IllegalStateException("海外安全投影消费失败: HTTP " + response.statusCode());
|
||||
}
|
||||
return String.valueOf(response.statusCode());
|
||||
}
|
||||
}
|
||||
+40
-9
@@ -1,10 +1,12 @@
|
||||
package com.baiyue.eventbridge.service;
|
||||
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.eventbridge.config.EventDispatcherProperties;
|
||||
import com.baiyue.eventbridge.domain.BridgeDeliveryAttempt;
|
||||
import com.baiyue.eventbridge.domain.BridgeInbox;
|
||||
import com.baiyue.eventbridge.mapper.BridgeDeliveryAttemptMapper;
|
||||
import com.baiyue.eventbridge.mapper.BridgeInboxMapper;
|
||||
import com.baiyue.eventbridge.metrics.EventBridgeMetrics;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
@@ -12,6 +14,7 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
@@ -21,7 +24,9 @@ public class OverseasEventDispatcher {
|
||||
@Resource private BridgeDeliveryAttemptMapper attemptMapper;
|
||||
@Resource private LastlegEventClient eventClient;
|
||||
@Resource private FirstlegEventClient firstlegEventClient;
|
||||
@Resource private SecurityProjectionEventClient securityProjectionEventClient;
|
||||
@Resource private EventDispatcherProperties properties;
|
||||
@Resource private EventBridgeMetrics metrics;
|
||||
private final String leaseOwner = ManagementFactory.getRuntimeMXBean().getName() + ":" + UUID.randomUUID();
|
||||
|
||||
@Scheduled(fixedDelayString = "${baiyue.event-dispatcher.fixed-delay:3000}")
|
||||
@@ -40,26 +45,52 @@ public class OverseasEventDispatcher {
|
||||
int retryAttempt = event.getRetryCount() == null ? 1 : event.getRetryCount() + 1;
|
||||
int deliveryAttemptNo = inboxMapper.nextAttemptNo(eventId);
|
||||
BridgeDeliveryAttempt attempt = startAttempt(event, deliveryAttemptNo);
|
||||
String responseCode;
|
||||
try {
|
||||
String responseCode = deliver(event);
|
||||
if (inboxMapper.markProcessed(eventId, leaseOwner) != 1) {
|
||||
throw new IllegalStateException("事件租约已失效,无法确认投递结果");
|
||||
}
|
||||
finishAttempt(attempt, "DELIVERED", responseCode, null);
|
||||
responseCode = deliver(event);
|
||||
} catch (Exception exception) {
|
||||
String error = abbreviate(exception.getMessage(), 1000);
|
||||
inboxMapper.markFailed(eventId, leaseOwner, properties.maxAttempts(), backoffSeconds(retryAttempt), error);
|
||||
finishAttempt(attempt, retryAttempt >= properties.maxAttempts() ? "DEAD" : "RETRY", null, error);
|
||||
int marked = inboxMapper.markFailed(eventId, leaseOwner, properties.maxAttempts(),
|
||||
backoffSeconds(retryAttempt), error);
|
||||
String attemptStatus = marked == 1
|
||||
? (retryAttempt >= properties.maxAttempts() ? "DEAD" : "RETRY")
|
||||
: "FAILED_LEASE_LOST";
|
||||
finishAttempt(attempt, attemptStatus, null, error);
|
||||
metrics.recordTransport(event, EventBridgeMetrics.TransportResult.valueOf(attemptStatus));
|
||||
log.warn("跨区事件投递失败 eventId={}, retryAttempt={}, deliveryAttempt={}, reason={}",
|
||||
eventId, retryAttempt, deliveryAttemptNo, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (inboxMapper.markProcessed(eventId, leaseOwner) != 1) {
|
||||
String error = "下游已确认成功,但事件租约已失效;保留当前 Inbox 状态并由新租约持有者幂等续投";
|
||||
finishAttempt(attempt, "ACKNOWLEDGED_LEASE_LOST", responseCode, error);
|
||||
metrics.recordTransport(event, EventBridgeMetrics.TransportResult.ACKNOWLEDGED_LEASE_LOST);
|
||||
log.warn("跨区事件成功确认时租约已失效 eventId={}, deliveryAttempt={}", eventId, deliveryAttemptNo);
|
||||
return;
|
||||
}
|
||||
finishAttempt(attempt, "DELIVERED", responseCode, null);
|
||||
metrics.recordTransport(event, EventBridgeMetrics.TransportResult.DELIVERED);
|
||||
}
|
||||
|
||||
private String deliver(BridgeInbox event) throws Exception {
|
||||
return "CN".equals(event.getTargetRegion())
|
||||
? firstlegEventClient.deliver(event) : eventClient.deliver(event);
|
||||
String eventType = event.getEventType();
|
||||
if (eventType == null || eventType.isBlank()) {
|
||||
throw new IllegalArgumentException("事件类型不能为空,无法选择下游消费者");
|
||||
}
|
||||
return switch (event.getTargetRegion()) {
|
||||
case "CN" -> firstlegEventClient.deliver(event);
|
||||
case "OVERSEAS" -> SECURITY_EVENT_TYPES.contains(eventType)
|
||||
? securityProjectionEventClient.deliver(event) : eventClient.deliver(event);
|
||||
default -> throw new IllegalArgumentException("不支持的事件目标区域: " + event.getTargetRegion());
|
||||
};
|
||||
}
|
||||
|
||||
private static final Set<String> SECURITY_EVENT_TYPES = Set.of(
|
||||
EventTypes.IAM_USER_STATUS_CHANGED,
|
||||
EventTypes.IAM_TENANT_STATUS_CHANGED,
|
||||
EventTypes.IAM_PERMISSION_VERSION_CHANGED);
|
||||
|
||||
private BridgeDeliveryAttempt startAttempt(BridgeInbox event, int attemptNo) {
|
||||
BridgeDeliveryAttempt attempt = new BridgeDeliveryAttempt();
|
||||
attempt.setEventId(event.getEventId());
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.baiyue.eventbridge.service;
|
||||
|
||||
import com.baiyue.eventbridge.domain.BridgeInbox;
|
||||
|
||||
public interface SecurityProjectionEventClient {
|
||||
String deliver(BridgeInbox event) throws Exception;
|
||||
}
|
||||
Reference in New Issue
Block a user