feat: enforce canonical container identity across regions
This commit is contained in:
+31
-6
@@ -45,6 +45,11 @@ public class EventInboxService {
|
||||
if (existing != null) {
|
||||
return duplicateOrConflict(existing, envelope);
|
||||
}
|
||||
String idempotencyKey = idempotencyKey(envelope);
|
||||
BridgeInbox sameBusinessEvent = inboxStore.findByIdempotencyKey(idempotencyKey);
|
||||
if (sameBusinessEvent != null) {
|
||||
return duplicateBusinessEventOrConflict(sameBusinessEvent, envelope);
|
||||
}
|
||||
BridgeInbox inbox = toInbox(envelope);
|
||||
try {
|
||||
inboxStore.insert(inbox);
|
||||
@@ -52,13 +57,25 @@ public class EventInboxService {
|
||||
return new EventReceiveResult(envelope.eventId(), "已接收", inbox.getReceivedAt());
|
||||
} catch (DuplicateKeyException exception) {
|
||||
BridgeInbox concurrent = inboxStore.findByEventId(envelope.eventId());
|
||||
if (concurrent == null) concurrent = inboxStore.findByIdempotencyKey(idempotencyKey);
|
||||
if (concurrent == null) {
|
||||
throw exception;
|
||||
}
|
||||
return duplicateOrConflict(concurrent, envelope);
|
||||
return Objects.equals(concurrent.getEventId(), envelope.eventId())
|
||||
? duplicateOrConflict(concurrent, envelope)
|
||||
: duplicateBusinessEventOrConflict(concurrent, envelope);
|
||||
}
|
||||
}
|
||||
|
||||
private EventReceiveResult duplicateBusinessEventOrConflict(BridgeInbox existing, EventEnvelope envelope) {
|
||||
if (!sameBusinessEvent(existing, envelope)) {
|
||||
metrics.recordReceive(envelope, EventBridgeMetrics.ReceiveResult.CONFLICT);
|
||||
throw new IllegalArgumentException("相同eventKey对应的事件负载不一致,拒绝柜号或事实漂移");
|
||||
}
|
||||
metrics.recordReceive(envelope, EventBridgeMetrics.ReceiveResult.DUPLICATE);
|
||||
return new EventReceiveResult(existing.getEventId(), "重复", existing.getReceivedAt());
|
||||
}
|
||||
|
||||
private EventReceiveResult duplicateOrConflict(BridgeInbox existing, EventEnvelope envelope) {
|
||||
if (!sameEnvelope(existing, envelope)) {
|
||||
metrics.recordReceive(envelope, EventBridgeMetrics.ReceiveResult.CONFLICT);
|
||||
@@ -69,6 +86,13 @@ public class EventInboxService {
|
||||
}
|
||||
|
||||
private boolean sameEnvelope(BridgeInbox existing, EventEnvelope envelope) {
|
||||
return sameBusinessEvent(existing, envelope)
|
||||
&& Objects.equals(normalizeOccurredAt(existing.getOccurredAt()), normalizeOccurredAt(envelope.occurredAt()))
|
||||
&& Objects.equals(existing.getTraceId(), envelope.traceId())
|
||||
&& Objects.equals(existing.getCausationId(), envelope.causationId());
|
||||
}
|
||||
|
||||
private boolean sameBusinessEvent(BridgeInbox existing, EventEnvelope envelope) {
|
||||
return existing.getPayloadHash() != null
|
||||
&& existing.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())
|
||||
&& Objects.equals(existing.getEventType(), envelope.eventType())
|
||||
@@ -80,16 +104,13 @@ public class EventInboxService {
|
||||
&& Objects.equals(existing.getAggregateId(), envelope.aggregateId())
|
||||
&& Objects.equals(existing.getAggregateVersion(), envelope.aggregateVersion())
|
||||
&& Objects.equals(existing.getHandoverVersion(), envelope.handoverVersion())
|
||||
&& Objects.equals(normalizeOccurredAt(existing.getOccurredAt()), normalizeOccurredAt(envelope.occurredAt()))
|
||||
&& Objects.equals(existing.getTraceId(), envelope.traceId())
|
||||
&& Objects.equals(existing.getCorrelationId(), envelope.correlationId())
|
||||
&& Objects.equals(existing.getCausationId(), envelope.causationId());
|
||||
&& Objects.equals(existing.getCorrelationId(), envelope.correlationId());
|
||||
}
|
||||
|
||||
private BridgeInbox toInbox(EventEnvelope envelope) {
|
||||
BridgeInbox inbox = new BridgeInbox();
|
||||
inbox.setEventId(envelope.eventId());
|
||||
inbox.setIdempotencyKey(envelope.sourceRegion() + ":" + envelope.eventId());
|
||||
inbox.setIdempotencyKey(idempotencyKey(envelope));
|
||||
inbox.setEventType(envelope.eventType());
|
||||
inbox.setEventVersion(envelope.eventVersion());
|
||||
inbox.setSourceRegion(envelope.sourceRegion().name());
|
||||
@@ -117,4 +138,8 @@ public class EventInboxService {
|
||||
private static Instant normalizeOccurredAt(Instant value) {
|
||||
return value == null ? null : value.truncatedTo(ChronoUnit.MILLIS);
|
||||
}
|
||||
|
||||
private static String idempotencyKey(EventEnvelope envelope) {
|
||||
return envelope.sourceRegion() + ":" + envelope.correlationId();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ import com.baiyue.eventbridge.domain.BridgeInbox;
|
||||
*/
|
||||
public interface InboxStore {
|
||||
BridgeInbox findByEventId(String eventId);
|
||||
BridgeInbox findByIdempotencyKey(String idempotencyKey);
|
||||
void insert(BridgeInbox inbox);
|
||||
}
|
||||
|
||||
+7
@@ -4,6 +4,7 @@ import com.baiyue.eventbridge.domain.BridgeInbox;
|
||||
import com.baiyue.eventbridge.mapper.BridgeInboxMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
|
||||
/**
|
||||
* 基于 MyBatis-Plus 的入站幂等存储。
|
||||
@@ -17,6 +18,12 @@ public class MybatisInboxStore implements InboxStore {
|
||||
return mapper.selectById(eventId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BridgeInbox findByIdempotencyKey(String idempotencyKey) {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<BridgeInbox>()
|
||||
.eq(BridgeInbox::getIdempotencyKey, idempotencyKey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insert(BridgeInbox inbox) {
|
||||
mapper.insert(inbox);
|
||||
|
||||
Reference in New Issue
Block a user