T1.5: 台账异步落库(ArrayBlockingQueue(10000) offer 不阻塞;满200/500ms 单线程批量 saveAll+按日 upsert;队满原子计数+ERROR 不抛;关停 flush 余量;hibernate batch_size=200)

This commit is contained in:
chenxing
2026-09-21 09:29:48 +08:00
parent 26f5787000
commit 28b9bfca3c
4 changed files with 386 additions and 0 deletions
@@ -0,0 +1,107 @@
package com.yunhai.platform.gateway.log;
import com.yunhai.platform.gateway.entity.OpenApiLog;
import com.yunhai.platform.gateway.entity.OpenApiStatDaily;
import com.yunhai.platform.gateway.repository.OpenApiLogRepository;
import com.yunhai.platform.gateway.repository.OpenApiStatDailyRepository;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* AccessLogService 三用例(T1.5 TDD):offer 入队成功路径(请求线程零写库);
* 满 200 条触发批量 flush(同步触发点测,不起真线程等待——注入同线程 dispatcher,
* 一批 JPA saveAll + 按日 upsert stat_daily);队列满 offer 失败 → 丢弃计数 +1 且绝不抛异常。
*/
class AccessLogServiceTest {
private AccessLogService service(int capacity, OpenApiLogRepository logRepo,
OpenApiStatDailyRepository statRepo) {
AccessLogService svc = new AccessLogService(capacity);
ReflectionTestUtils.setField(svc, "logRepository", logRepo);
ReflectionTestUtils.setField(svc, "statRepository", statRepo);
svc.setFlushDispatcher(Runnable::run); // 同步触发点:不真起线程
return svc;
}
private OpenApiLog entry(String target, String caller, int status) {
OpenApiLog e = new OpenApiLog();
e.setTs(LocalDateTime.now());
e.setTargetCode(target);
e.setCallerAppKey(caller);
e.setMethod("GET");
e.setPath("/oapi/x");
e.setStatus(status);
e.setCostMs(10);
return e;
}
@Test
void offer入队成功且请求线程零写库() {
OpenApiLogRepository logRepo = mock(OpenApiLogRepository.class);
OpenApiStatDailyRepository statRepo = mock(OpenApiStatDailyRepository.class);
AccessLogService svc = service(AccessLogService.QUEUE_CAPACITY, logRepo, statRepo);
svc.record(entry("usermsg", "100617", 200));
assertThat(svc.queueSize()).as("入队 1 条").isEqualTo(1);
assertThat(svc.droppedCount()).isZero();
verifyNoInteractions(logRepo, statRepo); // 红线:不得在请求线程直接写库
}
@Test
void 满200条触发批量flush并upsert日统计() {
OpenApiLogRepository logRepo = mock(OpenApiLogRepository.class);
OpenApiStatDailyRepository statRepo = mock(OpenApiStatDailyRepository.class);
AccessLogService svc = service(AccessLogService.QUEUE_CAPACITY, logRepo, statRepo);
for (int i = 0; i < AccessLogService.BATCH_SIZE; i++) {
svc.record(entry("usermsg", "100617", i == 199 ? 500 : 200)); // 末条 HTTP≥400 计 err
}
ArgumentCaptor<List<OpenApiLog>> batchCap = ArgumentCaptor.forClass(List.class);
verify(logRepo).saveAll(batchCap.capture());
assertThat(batchCap.getValue()).as("一批 200 条批量插入").hasSize(200);
assertThat(svc.queueSize()).as("flush 后队列清空").isZero();
ArgumentCaptor<OpenApiStatDaily> statCap = ArgumentCaptor.forClass(OpenApiStatDaily.class);
verify(statRepo).save(statCap.capture());
OpenApiStatDaily stat = statCap.getValue();
assertThat(stat.getTargetCode()).isEqualTo("usermsg");
assertThat(stat.getCallerAppKey()).isEqualTo("100617");
assertThat(stat.getTotal()).isEqualTo(200);
assertThat(stat.getErrTotal()).as("status≥400 计入 err").isEqualTo(1);
assertThat(stat.getAvgCostMs()).isEqualTo(10);
}
@Test
void 队列满丢弃计数加一且不抛异常() {
OpenApiLogRepository logRepo = mock(OpenApiLogRepository.class);
OpenApiStatDailyRepository statRepo = mock(OpenApiStatDailyRepository.class);
AccessLogService svc = service(2, logRepo, statRepo); // 测试容量:小队列复现背压
svc.record(entry("usermsg", "a", 200));
svc.record(entry("usermsg", "a", 200));
assertThat(svc.queueSize()).isEqualTo(2);
assertThatCode(() -> {
svc.record(entry("usermsg", "a", 200)); // 第 3 条 = 队列满
svc.record(entry("usermsg", "a", 200));
}).as("绝不阻塞/抛出,反杀业务不可接受").doesNotThrowAnyException();
assertThat(svc.droppedCount()).as("累计丢弃数").isEqualTo(2);
assertThat(svc.queueSize()).isEqualTo(2);
verify(logRepo, never()).saveAll(any());
}
}