优化
This commit is contained in:
+75
-37
@@ -2,55 +2,93 @@ package com.baiyue.lastleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.lastleg.entity.LastlegEventOutbox;
|
||||
import com.baiyue.lastleg.mapper.LastlegEventOutboxMapper;
|
||||
import com.baiyue.lastleg.financefee.repository.OverseasFeeOutboxRepository;
|
||||
import com.baiyue.common.core.outbox.EventDeliveryJournal;
|
||||
import com.baiyue.common.core.outbox.EventDeliveryJournal.Outcome;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class OutboxLeaseDatabaseTest {
|
||||
@Test void recoveredLeasesCountFailuresAndOldOwnersCannotChangeNewState() throws Exception {
|
||||
var dataSource = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID()
|
||||
+ ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
@Test void unloadCompletedOutboxRepositoryUsesAtomicAuditAndBackoff() throws Exception {
|
||||
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 ll_event_outbox(id BIGINT PRIMARY KEY,customer_id VARCHAR(64),aggregate_type VARCHAR(64),aggregate_id VARCHAR(64),event_type VARCHAR(64),event_version INT,event_key VARCHAR(160),payload_json TEXT,status VARCHAR(32),retry_count INT NOT NULL DEFAULT 0,next_retry_at TIMESTAMP,published_at TIMESTAMP,locked_at TIMESTAMP,locked_by VARCHAR(128),last_error VARCHAR(1000),created_at TIMESTAMP)");
|
||||
jdbc.execute("CREATE TABLE ll_event_outbox(id BIGINT PRIMARY KEY,customer_id VARCHAR(64),aggregate_type VARCHAR(64),aggregate_id VARCHAR(64),event_version INT,event_type VARCHAR(96),event_key VARCHAR(160),payload_json TEXT,status VARCHAR(32),retry_count INT NOT NULL DEFAULT 0,next_retry_at TIMESTAMP,published_at TIMESTAMP,locked_at TIMESTAMP,locked_by VARCHAR(128),last_error VARCHAR(1000),created_at TIMESTAMP)");
|
||||
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))");
|
||||
var configuration = new MybatisConfiguration();
|
||||
configuration.addMapper(LastlegEventOutboxMapper.class);
|
||||
var factory = new MybatisSqlSessionFactoryBean();
|
||||
factory.setDataSource(dataSource);
|
||||
factory.setConfiguration(configuration);
|
||||
try (var session = factory.getObject().openSession(true)) {
|
||||
var mapper = session.getMapper(LastlegEventOutboxMapper.class);
|
||||
var unload = new UnloadCompletedOutboxRepository();
|
||||
ReflectionTestUtils.setField(unload, "mapper", mapper);
|
||||
var fee = new OverseasFeeOutboxRepository(mapper);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (long id : new long[]{1, 2}) {
|
||||
jdbc.update("INSERT INTO ll_event_outbox(id,event_type,status,retry_count,next_retry_at,created_at) VALUES(?,?,'PENDING',0,?,?)",
|
||||
id, id == 1 ? EventTypes.OVERSEAS_CONTAINER_UNLOADED : EventTypes.BUSINESS_FEE_SOURCE_CHANGED,
|
||||
now.minusSeconds(1), now.minusMinutes(2));
|
||||
boolean first = id == 1 ? unload.claim(id, "old", now, now.minusSeconds(60)) : fee.claim(id, "old", now, now.minusSeconds(60));
|
||||
assertThat(first).isTrue();
|
||||
assertThat(jdbc.queryForObject("SELECT retry_count FROM ll_event_outbox WHERE id=?", Integer.class, id)).isZero();
|
||||
jdbc.update("UPDATE ll_event_outbox SET locked_at=? WHERE id=?", now.minusSeconds(61), id);
|
||||
boolean second = id == 1 ? unload.claim(id, "new", now, now.minusSeconds(60)) : fee.claim(id, "new", now, now.minusSeconds(60));
|
||||
assertThat(second).isTrue();
|
||||
assertThat(jdbc.queryForObject("SELECT retry_count FROM ll_event_outbox WHERE id=?", Integer.class, id)).isEqualTo(1);
|
||||
if (id == 1) unload.dead(id, "old", 12, "late failure"); else fee.dead(id, "old", 12, "late failure");
|
||||
assertThat(jdbc.queryForObject("SELECT status FROM ll_event_outbox WHERE id=?", String.class, id)).isEqualTo("PROCESSING");
|
||||
if (id == 1) unload.dead(id, "new", 12, "exhausted"); else fee.dead(id, "new", 12, "exhausted");
|
||||
assertThat(jdbc.queryForObject("SELECT status FROM ll_event_outbox WHERE id=?", String.class, id)).isEqualTo("DEAD");
|
||||
boolean third = id == 1 ? unload.claim(id, "third", now, now.minusSeconds(60)) : fee.claim(id, "third", now, now.minusSeconds(60));
|
||||
assertThat(third).isFalse();
|
||||
}
|
||||
jdbc.update("UPDATE ll_event_outbox SET status='PENDING',next_retry_at=? WHERE id=2", now.plusMinutes(5));
|
||||
assertThat(fee.claim(2L, "early", now, now.minusSeconds(60))).isFalse();
|
||||
}
|
||||
factory.setDataSource(dataSource); factory.setConfiguration(configuration);
|
||||
var mapper = new SqlSessionTemplate(factory.getObject()).getMapper(LastlegEventOutboxMapper.class);
|
||||
var repository = new UnloadCompletedOutboxRepository();
|
||||
ReflectionTestUtils.setField(repository, "mapper", mapper);
|
||||
var journal = new EventDeliveryJournal(jdbc, new DataSourceTransactionManager(dataSource));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
jdbc.update("INSERT INTO ll_event_outbox(id,event_type,status,retry_count,next_retry_at,created_at) VALUES(1,?,'PENDING',0,?,?)", "OverseasContainerUnloaded", now.minusSeconds(1), now.minusMinutes(3));
|
||||
assertThat(journal.begin("old", "lastleg-service", "OverseasContainerUnloaded", "1", null, "consumer", now.minusMinutes(2), () -> repository.claim(1L, "old", now.minusMinutes(2)))).isFalse();
|
||||
assertThat(journal.begin("old", "lastleg-service", "OverseasContainerUnloaded", "1", null, "consumer", now, () -> repository.claim(1L, "old", now))).isTrue();
|
||||
jdbc.update("UPDATE ll_event_outbox SET locked_at=? WHERE id=1", now.minusSeconds(61));
|
||||
assertThat(repository.claim(1L, "new", now)).isFalse();
|
||||
var next = now.plusSeconds(10);
|
||||
assertThat(journal.complete("old", Outcome.LEASE_EXPIRED, now, "LEASE_EXPIRED", next,
|
||||
() -> repository.expire(1L, "old", now.minusSeconds(60), 1, next, "lease expired"))).isTrue();
|
||||
assertThat(jdbc.queryForObject("SELECT retry_count FROM ll_event_outbox", Integer.class)).isEqualTo(1);
|
||||
assertThat(repository.claim(1L, "early", now)).isFalse();
|
||||
assertThat(journal.begin("new", "lastleg-service", "OverseasContainerUnloaded", "1", null, "consumer", next,
|
||||
() -> repository.claim(1L, "new", next))).isTrue();
|
||||
assertThat(journal.complete("old", Outcome.DEAD, next, "RETRY_EXHAUSTED", null,
|
||||
() -> repository.dead(1L, "old", 12, "late"))).isFalse();
|
||||
jdbc.execute("ALTER TABLE event_delivery_attempt ADD CONSTRAINT reject_applied CHECK(outcome <> 'APPLIED')");
|
||||
assertThatThrownBy(() -> journal.complete("new", Outcome.APPLIED, next, null, null,
|
||||
() -> repository.published(1L, "new", next))).isInstanceOf(RuntimeException.class);
|
||||
assertThat(jdbc.queryForObject("SELECT status FROM ll_event_outbox", String.class)).isEqualTo("PROCESSING");
|
||||
assertThat(jdbc.queryForObject("SELECT outcome FROM event_delivery_attempt WHERE attempt_id='new'", String.class)).isEqualTo("STARTED");
|
||||
jdbc.execute("ALTER TABLE event_delivery_attempt DROP CONSTRAINT reject_applied");
|
||||
assertThat(journal.complete("new", Outcome.APPLIED, next, null, null,
|
||||
() -> repository.published(1L, "new", next))).isTrue();
|
||||
assertThat(jdbc.queryForObject("SELECT status FROM ll_event_outbox", String.class)).isEqualTo("PUBLISHED");
|
||||
}
|
||||
@Test void overseasFeeOutboxRepositoryUsesAtomicAuditAndBackoff() throws Exception {
|
||||
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 ll_event_outbox(id BIGINT PRIMARY KEY,customer_id VARCHAR(64),aggregate_type VARCHAR(64),aggregate_id VARCHAR(64),event_version INT,event_type VARCHAR(96),event_key VARCHAR(160),payload_json TEXT,status VARCHAR(32),retry_count INT NOT NULL DEFAULT 0,next_retry_at TIMESTAMP,published_at TIMESTAMP,locked_at TIMESTAMP,locked_by VARCHAR(128),last_error VARCHAR(1000),created_at TIMESTAMP)");
|
||||
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))");
|
||||
var configuration = new MybatisConfiguration();
|
||||
configuration.addMapper(LastlegEventOutboxMapper.class);
|
||||
var factory = new MybatisSqlSessionFactoryBean();
|
||||
factory.setDataSource(dataSource); factory.setConfiguration(configuration);
|
||||
var mapper = new SqlSessionTemplate(factory.getObject()).getMapper(LastlegEventOutboxMapper.class);
|
||||
var repository = new com.baiyue.lastleg.financefee.repository.OverseasFeeOutboxRepository(mapper);
|
||||
var journal = new EventDeliveryJournal(jdbc, new DataSourceTransactionManager(dataSource));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
jdbc.update("INSERT INTO ll_event_outbox(id,event_type,status,retry_count,next_retry_at,created_at) VALUES(1,?,'PENDING',0,?,?)", "BusinessFeeSourceChanged", now.minusSeconds(1), now.minusMinutes(3));
|
||||
assertThat(journal.begin("old", "lastleg-service", "BusinessFeeSourceChanged", "1", null, "consumer", now.minusMinutes(2), () -> repository.claim(1L, "old", now.minusMinutes(2)))).isFalse();
|
||||
assertThat(journal.begin("old", "lastleg-service", "BusinessFeeSourceChanged", "1", null, "consumer", now, () -> repository.claim(1L, "old", now))).isTrue();
|
||||
jdbc.update("UPDATE ll_event_outbox SET locked_at=? WHERE id=1", now.minusSeconds(61));
|
||||
assertThat(repository.claim(1L, "new", now)).isFalse();
|
||||
var next = now.plusSeconds(10);
|
||||
assertThat(journal.complete("old", Outcome.LEASE_EXPIRED, now, "LEASE_EXPIRED", next,
|
||||
() -> repository.expire(1L, "old", now.minusSeconds(60), 1, next, "lease expired"))).isTrue();
|
||||
assertThat(jdbc.queryForObject("SELECT retry_count FROM ll_event_outbox", Integer.class)).isEqualTo(1);
|
||||
assertThat(repository.claim(1L, "early", now)).isFalse();
|
||||
assertThat(journal.begin("new", "lastleg-service", "BusinessFeeSourceChanged", "1", null, "consumer", next,
|
||||
() -> repository.claim(1L, "new", next))).isTrue();
|
||||
assertThat(journal.complete("old", Outcome.DEAD, next, "RETRY_EXHAUSTED", null,
|
||||
() -> repository.dead(1L, "old", 12, "late"))).isFalse();
|
||||
jdbc.execute("ALTER TABLE event_delivery_attempt ADD CONSTRAINT reject_applied CHECK(outcome <> 'APPLIED')");
|
||||
assertThatThrownBy(() -> journal.complete("new", Outcome.APPLIED, next, null, null,
|
||||
() -> repository.published(1L, "new", next))).isInstanceOf(RuntimeException.class);
|
||||
assertThat(jdbc.queryForObject("SELECT status FROM ll_event_outbox", String.class)).isEqualTo("PROCESSING");
|
||||
assertThat(jdbc.queryForObject("SELECT outcome FROM event_delivery_attempt WHERE attempt_id='new'", String.class)).isEqualTo("STARTED");
|
||||
jdbc.execute("ALTER TABLE event_delivery_attempt DROP CONSTRAINT reject_applied");
|
||||
assertThat(journal.complete("new", Outcome.APPLIED, next, null, null,
|
||||
() -> repository.published(1L, "new", next))).isTrue();
|
||||
assertThat(jdbc.queryForObject("SELECT status FROM ll_event_outbox", String.class)).isEqualTo("PUBLISHED");
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -27,8 +27,8 @@ class UnloadCompletedOutboxRepositoryTest {
|
||||
ArgumentCaptor<LambdaUpdateWrapper> wrapper = ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
|
||||
when(mapper.update(isNull(LastlegEventOutbox.class), wrapper.capture())).thenReturn(1);
|
||||
LocalDateTime now = LocalDateTime.of(2026, 9, 29, 12, 0);
|
||||
assertThat(repository.claim(1L, "attempt", now, now.minusMinutes(1))).isTrue();
|
||||
assertThat(wrapper.getValue().getSqlSegment()).contains("next_retry_at", "<=", "status", "locked_at");
|
||||
assertThat(repository.claim(1L, "attempt", now)).isTrue();
|
||||
assertThat(wrapper.getValue().getSqlSegment()).contains("next_retry_at", "<=", "status").doesNotContain("locked_at");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -43,9 +43,9 @@ class UnloadCompletedOutboxRepositoryTest {
|
||||
when(mapper.update(isNull(LastlegEventOutbox.class), wrapper.capture())).thenReturn(1);
|
||||
LocalDateTime now = LocalDateTime.of(2026, 8, 25, 12, 0);
|
||||
|
||||
assertThat(repository.claim(1L, "worker", now, now.minusMinutes(1))).isTrue();
|
||||
assertThat(repository.claim(1L, "worker", now)).isTrue();
|
||||
|
||||
assertThat(wrapper.getValue().getSqlSegment())
|
||||
.contains("next_retry_at", "<=", "status", "locked_at");
|
||||
.contains("next_retry_at", "<=", "status").doesNotContain("locked_at");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user