This commit is contained in:
2026-09-29 18:47:37 +08:00
parent 2c60a29e0f
commit c2927ab9a6
291 changed files with 4008 additions and 6298 deletions
@@ -7,7 +7,6 @@ import com.baiyue.common.event.EventPayloadCanonicalizer;
import com.baiyue.common.event.EventPayloadHash;
import com.baiyue.common.event.EventTypes;
import com.baiyue.common.event.OverseasContainerUnloadedV1;
import com.baiyue.common.event.RegionCode;
import com.baiyue.lastleg.client.DirectEventClient;
import com.baiyue.lastleg.entity.LastlegEventOutbox;
import com.baiyue.lastleg.repository.UnloadCompletedOutboxRepository;
@@ -23,6 +22,11 @@ import org.springframework.test.util.ReflectionTestUtils;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import com.baiyue.common.core.outbox.EventDeliveryJournal;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@@ -35,11 +39,21 @@ class InboundUnloadCompletedOutboxRelayTest {
private ObjectMapper objectMapper;
private InboundUnloadCompletedOutboxRelay relay;
private EventDeliveryJournal journal;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
relay = new InboundUnloadCompletedOutboxRelay();
ReflectionTestUtils.setField(relay, "repository", repository);
var dataSource = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
var jdbc = new JdbcTemplate(dataSource);
jdbc.execute("CREATE TABLE event_delivery_attempt(attempt_id VARCHAR(36) PRIMARY KEY,service_name VARCHAR(32) NOT NULL,event_type VARCHAR(96) NOT NULL,entry_id VARCHAR(128) NOT NULL,event_id VARCHAR(128),target_name VARCHAR(64) NOT NULL,started_at TIMESTAMP(3) NOT NULL,finished_at TIMESTAMP(3),duration_ms BIGINT,outcome VARCHAR(24) NOT NULL,error_code VARCHAR(64),next_retry_at TIMESTAMP(3))");
journal = new EventDeliveryJournal(jdbc, new DataSourceTransactionManager(dataSource));
ReflectionTestUtils.setField(relay, "journal", journal);
lenient().when(repository.published(anyLong(), anyString(), any())).thenReturn(1);
lenient().when(repository.retry(anyLong(), anyString(), any(), anyString())).thenReturn(1);
lenient().when(repository.dead(anyLong(), anyString(), anyInt(), anyString())).thenReturn(1);
ReflectionTestUtils.setField(relay, "client", client);
ReflectionTestUtils.setField(relay, "objectMapper", objectMapper);
ReflectionTestUtils.setField(relay, "internalToken", "internal-token");
@@ -52,7 +66,7 @@ class InboundUnloadCompletedOutboxRelayTest {
EventEnvelope event = event();
LastlegEventOutbox row = row(event);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
when(repository.claim(eq(9001L), anyString(), any())).thenReturn(true);
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
when(client.receive(eq("internal-token"), any(EventEnvelope.class)))
.thenReturn(ApiResponse.success(DeliveryAck.of(event, DeliveryAck.Outcome.APPLIED)));
@@ -68,7 +82,7 @@ class InboundUnloadCompletedOutboxRelayTest {
EventEnvelope event = event();
LastlegEventOutbox row = row(event);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
when(repository.claim(eq(9001L), anyString(), any())).thenReturn(true);
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
when(client.receive(eq("internal-token"), any(EventEnvelope.class)))
.thenThrow(new IllegalStateException("consumer unavailable"));
@@ -84,7 +98,7 @@ class InboundUnloadCompletedOutboxRelayTest {
EventEnvelope event = event();
LastlegEventOutbox row = row(event);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
when(repository.claim(eq(9001L), anyString(), any())).thenReturn(true);
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
when(client.receive(eq("internal-token"), any(EventEnvelope.class)))
.thenReturn(ApiResponse.failure(ResultCode.BAD_REQUEST,
@@ -102,7 +116,7 @@ class InboundUnloadCompletedOutboxRelayTest {
EventEnvelope event = event();
LastlegEventOutbox row = row(event);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
when(repository.claim(eq(9001L), anyString(), any())).thenReturn(true);
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
when(client.receive(eq("internal-token"), any(EventEnvelope.class)))
.thenReturn(ApiResponse.success(new DeliveryAck(event.eventId(), "sha256:" + "0".repeat(64), DeliveryAck.Outcome.APPLIED)))
@@ -118,7 +132,7 @@ class InboundUnloadCompletedOutboxRelayTest {
LastlegEventOutbox row = row(event());
row.setRetryCount(11);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
when(repository.claim(eq(9001L), anyString(), any())).thenReturn(true);
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
when(client.receive(anyString(), any())).thenThrow(new IllegalStateException("consumer unavailable"));
relay.relay();
@@ -132,7 +146,7 @@ class InboundUnloadCompletedOutboxRelayTest {
LastlegEventOutbox row = row(event());
row.setRetryCount(12);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
when(repository.claim(eq(9001L), anyString(), any())).thenReturn(true);
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
relay.relay();
verifyNoInteractions(client);
@@ -140,6 +154,19 @@ class InboundUnloadCompletedOutboxRelayTest {
verify(repository, never()).retry(anyLong(), anyString(), any(), anyString());
}
@Test
void expiredLeaseIsAuditedAndBackedOffWithoutImmediateHttp() throws Exception {
var row = row(event());
row.setStatus("PROCESSING"); row.setLockedBy("old"); row.setRetryCount(1);
journal.begin("old", "lastleg", row.getEventType(), "9001", null, "consumer", LocalDateTime.now().minusMinutes(2), () -> true);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
when(repository.expire(eq(9001L), eq("old"), any(), eq(2), any(), anyString())).thenReturn(1);
relay.relay();
verify(repository).expire(eq(9001L), eq("old"), any(), eq(2), argThat(next -> next.isAfter(LocalDateTime.now())), anyString());
verify(repository, never()).claim(anyLong(), anyString(), any());
verifyNoInteractions(client);
}
private LastlegEventOutbox row(EventEnvelope event) throws Exception {
LastlegEventOutbox row = new LastlegEventOutbox();
row.setId(9001L);
@@ -153,7 +180,7 @@ class InboundUnloadCompletedOutboxRelayTest {
Instant.parse("2026-08-20T03:00:00Z"), Instant.parse("2026-08-20T02:30:00Z"), null));
return new EventEnvelope("00000000-0000-4000-8000-000000000009",
EventTypes.OVERSEAS_CONTAINER_UNLOADED, 1, Instant.parse("2026-08-20T03:00:01Z"),
RegionCode.OVERSEAS, RegionCode.CN, "1", "CONTAINER_BOOKING", "1001", 7L, 1L,
"1", "CONTAINER_BOOKING", "1001", 7L, 1L,
"trace-12345678", "correlation-12345678", null, payload,
EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload)));
}
@@ -116,7 +116,6 @@ class InboundUnloadCompletedOutboxServiceTest {
Instant.parse("2026-08-24T09:30:00Z"), Instant.parse("2026-08-24T08:00:00Z"), null);
EventEnvelope envelope = new EventEnvelope("123e4567-e89b-12d3-a456-426614174000",
EventTypes.OVERSEAS_CONTAINER_UNLOADED, 1, Instant.parse("2026-08-24T09:30:01Z"),
com.baiyue.common.event.RegionCode.OVERSEAS, com.baiyue.common.event.RegionCode.CN,
customerId, "CONTAINER_BOOKING", bookingId, 7, 1L,
"123e4567-e89b-12d3-a456-426614174001",
EventTypes.OVERSEAS_CONTAINER_UNLOADED + ':' + bookingId, null,