This commit is contained in:
2026-08-25 14:46:06 +08:00
parent 5abc7543ae
commit f325d57fc1
106 changed files with 3682 additions and 450 deletions
@@ -1,10 +1,13 @@
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 io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -12,6 +15,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import java.net.URI;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -21,10 +25,14 @@ import static org.mockito.Mockito.*;
class OverseasEventDispatcherTest {
private final BridgeInboxMapper inboxMapper = mock(BridgeInboxMapper.class);
private final BridgeDeliveryAttemptMapper attemptMapper = mock(BridgeDeliveryAttemptMapper.class);
private final LastlegEventClient eventClient = mock(LastlegEventClient.class);
private final FirstlegEventClient firstlegEventClient = mock(FirstlegEventClient.class);
private final com.baiyue.eventbridge.service.LastlegEventClient eventClient =
mock(com.baiyue.eventbridge.service.LastlegEventClient.class);
private final com.baiyue.eventbridge.service.FirstlegEventClient firstlegEventClient =
mock(com.baiyue.eventbridge.service.FirstlegEventClient.class);
private final SecurityProjectionEventClient securityProjectionEventClient = mock(SecurityProjectionEventClient.class);
private OverseasEventDispatcher dispatcher;
private BridgeInbox event;
private SimpleMeterRegistry registry;
@BeforeEach
void setUp() {
@@ -33,16 +41,21 @@ class OverseasEventDispatcherTest {
Duration.ofSeconds(30), Duration.ofSeconds(5), Duration.ofMinutes(1),
Duration.ofSeconds(3), Duration.ofSeconds(10));
dispatcher = new OverseasEventDispatcher();
registry = new SimpleMeterRegistry();
ReflectionTestUtils.setField(dispatcher, "inboxMapper", inboxMapper);
ReflectionTestUtils.setField(dispatcher, "attemptMapper", attemptMapper);
ReflectionTestUtils.setField(dispatcher, "eventClient", eventClient);
ReflectionTestUtils.setField(dispatcher, "firstlegEventClient", firstlegEventClient);
ReflectionTestUtils.setField(dispatcher, "securityProjectionEventClient", securityProjectionEventClient);
ReflectionTestUtils.setField(dispatcher, "properties", properties);
ReflectionTestUtils.setField(dispatcher, "metrics", new EventBridgeMetrics(registry));
event = new BridgeInbox();
event.setEventId("event-1");
event.setEventType(EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED);
event.setSourceRegion("CN");
event.setTargetRegion("OVERSEAS");
event.setRetryCount(0);
event.setReceivedAt(LocalDateTime.now().minusSeconds(5));
when(inboxMapper.claim(eq("event-1"), anyString(), anyLong())).thenReturn(1);
when(inboxMapper.selectById("event-1")).thenReturn(event);
when(inboxMapper.nextAttemptNo("event-1")).thenReturn(1);
@@ -56,27 +69,50 @@ class OverseasEventDispatcherTest {
dispatcher.dispatchOne("event-1");
verify(inboxMapper).markProcessed(eq("event-1"), anyString());
verify(eventClient).deliver(event);
verifyNoInteractions(securityProjectionEventClient);
assertThat(updatedAttempt().getStatus()).isEqualTo("DELIVERED");
assertThat(transportCount("DELIVERED")).isEqualTo(1.0);
assertThat(registry.get(EventBridgeMetrics.DELIVERY_LATENCY_METRIC).timer().count()).isEqualTo(1);
}
@Test
void failedDeliveryIsScheduledForRetry() throws Exception {
when(eventClient.deliver(event)).thenThrow(new IllegalStateException("network down"));
when(inboxMapper.markFailed(eq("event-1"), anyString(), eq(3), anyLong(), eq("network down")))
.thenReturn(1);
dispatcher.dispatchOne("event-1");
verify(inboxMapper).markFailed(eq("event-1"), anyString(), eq(3), anyLong(), eq("network down"));
verify(inboxMapper).markFailed(eq("event-1"), anyString(), eq(3), eq(5L), eq("network down"));
assertThat(updatedAttempt().getStatus()).isEqualTo("RETRY");
assertThat(transportCount("RETRY")).isEqualTo(1.0);
}
@Test
void finalFailureIsMovedToDeadState() throws Exception {
event.setRetryCount(2);
when(eventClient.deliver(event)).thenThrow(new IllegalStateException("still down"));
when(inboxMapper.markFailed(eq("event-1"), anyString(), eq(3), anyLong(), eq("still down")))
.thenReturn(1);
dispatcher.dispatchOne("event-1");
verify(inboxMapper).markFailed(eq("event-1"), anyString(), eq(3), eq(20L), eq("still down"));
assertThat(updatedAttempt().getStatus()).isEqualTo("DEAD");
assertThat(transportCount("DEAD")).isEqualTo(1.0);
}
@Test
void exponentialBackoffIsCappedAtConfiguredMaximum() throws Exception {
event.setRetryCount(4);
when(eventClient.deliver(event)).thenThrow(new IllegalStateException("still down"));
when(inboxMapper.markFailed(eq("event-1"), anyString(), eq(3), anyLong(), eq("still down")))
.thenReturn(1);
dispatcher.dispatchOne("event-1");
verify(inboxMapper).markFailed(eq("event-1"), anyString(), eq(3), eq(60L), eq("still down"));
}
@Test
@@ -92,6 +128,7 @@ class OverseasEventDispatcherTest {
@Test
void cnTargetIsDeliveredToFirstleg() throws Exception {
event.setEventType(EventTypes.OVERSEAS_CONTAINER_UNLOADED);
event.setSourceRegion("OVERSEAS");
event.setTargetRegion("CN");
when(firstlegEventClient.deliver(event)).thenReturn("200");
@@ -101,9 +138,87 @@ class OverseasEventDispatcherTest {
verify(firstlegEventClient).deliver(event);
verifyNoInteractions(eventClient);
verifyNoInteractions(securityProjectionEventClient);
assertThat(updatedAttempt().getStatus()).isEqualTo("DELIVERED");
}
@Test
void iamSecurityEventsAreDeliveredToSecurityProjectionAndNeverLastleg() throws Exception {
for (String eventType : List.of(EventTypes.IAM_USER_STATUS_CHANGED,
EventTypes.IAM_TENANT_STATUS_CHANGED, EventTypes.IAM_PERMISSION_VERSION_CHANGED)) {
reset(inboxMapper, attemptMapper, eventClient, firstlegEventClient, securityProjectionEventClient);
event.setEventType(eventType);
when(inboxMapper.claim(eq("event-1"), anyString(), anyLong())).thenReturn(1);
when(inboxMapper.selectById("event-1")).thenReturn(event);
when(inboxMapper.nextAttemptNo("event-1")).thenReturn(1);
when(inboxMapper.markProcessed(eq("event-1"), anyString())).thenReturn(1);
when(securityProjectionEventClient.deliver(event)).thenReturn("200");
dispatcher.dispatchOne("event-1");
verify(securityProjectionEventClient).deliver(event);
verifyNoInteractions(eventClient);
verifyNoInteractions(firstlegEventClient);
}
}
@Test
void unsupportedTargetRegionIsNeverMisroutedToLastleg() {
event.setTargetRegion("UNKNOWN");
when(inboxMapper.markFailed(eq("event-1"), anyString(), eq(3), anyLong(), contains("不支持")))
.thenReturn(1);
dispatcher.dispatchOne("event-1");
verifyNoInteractions(eventClient);
verifyNoInteractions(firstlegEventClient);
verifyNoInteractions(securityProjectionEventClient);
assertThat(updatedAttempt().getStatus()).isEqualTo("RETRY");
}
@Test
void missingEventTypeFailsWithExplicitDiagnosticAndIsNeverDelivered() {
event.setEventType(null);
when(inboxMapper.markFailed(eq("event-1"), anyString(), eq(3), anyLong(),
eq("事件类型不能为空,无法选择下游消费者"))).thenReturn(1);
dispatcher.dispatchOne("event-1");
verifyNoInteractions(eventClient);
verifyNoInteractions(firstlegEventClient);
verifyNoInteractions(securityProjectionEventClient);
BridgeDeliveryAttempt attempt = updatedAttempt();
assertThat(attempt.getStatus()).isEqualTo("RETRY");
assertThat(attempt.getErrorMessage()).isEqualTo("事件类型不能为空,无法选择下游消费者");
}
@Test
void acknowledgedDeliveryWithLostLeaseIsNotRecordedAsRetryOrDead() throws Exception {
when(eventClient.deliver(event)).thenReturn("200");
when(inboxMapper.markProcessed(eq("event-1"), anyString())).thenReturn(0);
dispatcher.dispatchOne("event-1");
verify(inboxMapper, never()).markFailed(anyString(), anyString(), anyInt(), anyLong(), anyString());
BridgeDeliveryAttempt attempt = updatedAttempt();
assertThat(attempt.getStatus()).isEqualTo("ACKNOWLEDGED_LEASE_LOST");
assertThat(attempt.getResponseCode()).isEqualTo("200");
assertThat(attempt.getErrorMessage()).contains("租约已失效");
assertThat(transportCount("ACKNOWLEDGED_LEASE_LOST")).isEqualTo(1.0);
}
@Test
void failedDeliveryAfterLeaseTakeoverDoesNotOverwriteNewOwnerState() throws Exception {
when(eventClient.deliver(event)).thenThrow(new IllegalStateException("network down"));
when(inboxMapper.markFailed(eq("event-1"), anyString(), eq(3), anyLong(), eq("network down")))
.thenReturn(0);
dispatcher.dispatchOne("event-1");
assertThat(updatedAttempt().getStatus()).isEqualTo("FAILED_LEASE_LOST");
assertThat(transportCount("FAILED_LEASE_LOST")).isEqualTo(1.0);
}
@Test
void downstream503RetriesThenDeliversWithDiagnosticAttemptsInBothDirections() throws Exception {
for (String target : List.of("OVERSEAS", "CN")) {
@@ -162,4 +277,11 @@ class OverseasEventDispatcherTest {
verify(attemptMapper).updateById(captor.capture());
return captor.getValue();
}
private double transportCount(String result) {
return registry.get(EventBridgeMetrics.TRANSPORT_METRIC)
.tags("target_region", event.getTargetRegion(),
"event_type", event.getEventType(), "result", result)
.counter().count();
}
}