1. 提高 databus api 的网络失败重试次数,避免复用旧链接导致的 connection reset 错误
2. 兼容顶级组织同步时的组织编码生成逻辑
This commit is contained in:
@@ -38,7 +38,7 @@ public class GatewayWebClientConfiguration {
|
||||
|
||||
private ReactorClientHttpConnector buildConnector() {
|
||||
ConnectionProvider.Builder providerBuilder = ConnectionProvider.builder("databus-gateway")
|
||||
.maxIdleTime(Duration.ofMillis(maxIdleTimeMillis));
|
||||
.maxIdleTime(Duration.ofMillis(maxIdleTimeMillis));
|
||||
if (evictInBackgroundMillis > 0) {
|
||||
providerBuilder.evictInBackground(Duration.ofMillis(evictInBackgroundMillis));
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ public class HttpStepHandler implements ApiStepHandler {
|
||||
private final ExpressionExecutor expressionExecutor;
|
||||
|
||||
private static final Duration RETRY_DELAY = Duration.ofMillis(200);
|
||||
private static final int RETRY_ATTEMPTS = 3;
|
||||
|
||||
private static final Set<String> DEFAULT_FORWARDED_HEADERS = Set.of(
|
||||
"authorization",
|
||||
@@ -388,7 +389,7 @@ public class HttpStepHandler implements ApiStepHandler {
|
||||
}
|
||||
|
||||
private Mono<Object> applyResilientRetry(Mono<Object> responseMono, ApiStepDefinition stepDefinition) {
|
||||
return responseMono.retryWhen(Retry.fixedDelay(1, RETRY_DELAY)
|
||||
return responseMono.retryWhen(Retry.fixedDelay(RETRY_ATTEMPTS, RETRY_DELAY)
|
||||
.filter(this::isRetryableException)
|
||||
.doBeforeRetry(signal -> {
|
||||
if (log.isWarnEnabled()) {
|
||||
|
||||
@@ -11,32 +11,57 @@ import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
import reactor.netty.resources.ConnectionProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketException;
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
class HttpStepHandlerTest {
|
||||
|
||||
private MockWebServer server;
|
||||
private ExpressionExecutor expressionExecutor;
|
||||
private HttpStepHandler handler;
|
||||
private ConnectionProvider connectionProvider;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
server = new MockWebServer();
|
||||
server.start();
|
||||
expressionExecutor = Mockito.mock(ExpressionExecutor.class);
|
||||
handler = new HttpStepHandler(WebClient.builder(), expressionExecutor);
|
||||
connectionProvider = ConnectionProvider.builder("http-step-handler-test")
|
||||
.maxConnections(1)
|
||||
.maxIdleTime(Duration.ofMinutes(2))
|
||||
.pendingAcquireMaxCount(-1)
|
||||
.build();
|
||||
HttpClient httpClient = HttpClient.create(connectionProvider);
|
||||
WebClient.Builder builder = WebClient.builder()
|
||||
.clientConnector(new ReactorClientHttpConnector(httpClient));
|
||||
handler = new HttpStepHandler(builder, expressionExecutor);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
if (connectionProvider != null) {
|
||||
connectionProvider.disposeLater().block(Duration.ofSeconds(5));
|
||||
}
|
||||
server.shutdown();
|
||||
}
|
||||
|
||||
@@ -102,4 +127,44 @@ class HttpStepHandlerTest {
|
||||
assertThat(request.getHeader("Content-Type")).contains("application/json");
|
||||
assertThat(request.getBody().readUtf8()).contains("\"amount\":100");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRecoverWhenReusingStaleConnectionAfterIdle() {
|
||||
AtomicInteger attemptCounter = new AtomicInteger();
|
||||
ExchangeFunction exchangeFunction = request -> {
|
||||
int attempt = attemptCounter.incrementAndGet();
|
||||
if (attempt <= 2) {
|
||||
return Mono.error(new IOException("Simulated connection reset", new SocketException("Connection reset")));
|
||||
}
|
||||
return Mono.just(ClientResponse.create(HttpStatus.OK)
|
||||
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.body("{\"ok\":true}")
|
||||
.build());
|
||||
};
|
||||
|
||||
HttpStepHandler simulatedHandler = new HttpStepHandler(WebClient.builder().exchangeFunction(exchangeFunction), expressionExecutor);
|
||||
|
||||
ApiStepDO stepDO = new ApiStepDO();
|
||||
stepDO.setId(3L);
|
||||
stepDO.setType("HTTP");
|
||||
stepDO.setTargetEndpoint("POST http://idle-reset.test/stale-connection");
|
||||
|
||||
ApiStepDefinition stepDefinition = ApiStepDefinition.builder()
|
||||
.step(stepDO)
|
||||
.metadata(Collections.emptyMap())
|
||||
.transforms(Collections.emptyList())
|
||||
.build();
|
||||
|
||||
var stepHandler = simulatedHandler.build(null, stepDefinition);
|
||||
|
||||
ApiInvocationContext context = ApiInvocationContext.create();
|
||||
context.setRequestBody(Collections.singletonMap("foo", "bar"));
|
||||
|
||||
assertThatCode(() -> stepHandler.handle(context, new MessageHeaders(Collections.emptyMap())))
|
||||
.doesNotThrowAnyException();
|
||||
|
||||
assertThat(attemptCounter.get()).isEqualTo(3);
|
||||
assertThat(context.getStepResults()).isNotEmpty();
|
||||
assertThat(context.getStepResults().get(0).isSuccess()).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode USER_REGISTER_DISABLED = new ErrorCode(1_002_003_011, "注册功能已关闭");
|
||||
|
||||
// ========== 部门模块 1-002-004-000 ==========
|
||||
ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "已经存在该名字的部门");
|
||||
ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1_002_004_000, "当前上级部门已存在同名子部门");
|
||||
ErrorCode DEPT_PARENT_NOT_EXITS = new ErrorCode(1_002_004_001,"父级部门不存在");
|
||||
ErrorCode DEPT_NOT_FOUND = new ErrorCode(1_002_004_002, "机构不存在或当前账号无权限修改");
|
||||
ErrorCode DEPT_EXITS_CHILDREN = new ErrorCode(1_002_004_003, "存在子部门,无法删除");
|
||||
|
||||
@@ -71,9 +71,16 @@ public class DeptServiceImpl implements DeptService {
|
||||
// 校验部门名的唯一性
|
||||
validateDeptNameUnique(null, createReqVO.getParentId(), createReqVO.getName());
|
||||
// 生成并校验部门编码
|
||||
String generatedCode = generateDeptCode(createReqVO.getParentId());
|
||||
createReqVO.setCode(generatedCode);
|
||||
validateDeptCodeUnique(null, generatedCode);
|
||||
Long effectiveParentId = normalizeParentId(createReqVO.getParentId());
|
||||
boolean isTopLevel = Objects.equals(effectiveParentId, DeptDO.PARENT_ID_ROOT);
|
||||
String resolvedCode;
|
||||
if (isTopLevel) {
|
||||
resolvedCode = resolveTopLevelCode(null, createReqVO.getCode());
|
||||
} else {
|
||||
resolvedCode = generateDeptCode(effectiveParentId);
|
||||
validateDeptCodeUnique(null, resolvedCode);
|
||||
}
|
||||
createReqVO.setCode(resolvedCode);
|
||||
|
||||
// 插入部门
|
||||
DeptDO dept = BeanUtils.toBean(createReqVO, DeptDO.class);
|
||||
@@ -104,10 +111,25 @@ public class DeptServiceImpl implements DeptService {
|
||||
Long oldParentId = normalizeParentId(originalDept.getParentId());
|
||||
boolean parentChanged = !Objects.equals(newParentId, oldParentId);
|
||||
if (parentChanged) {
|
||||
String newCode = generateDeptCode(updateReqVO.getParentId());
|
||||
String newCode;
|
||||
if (Objects.equals(newParentId, DeptDO.PARENT_ID_ROOT)) {
|
||||
newCode = resolveTopLevelCode(updateReqVO.getId(), updateReqVO.getCode());
|
||||
} else {
|
||||
newCode = generateDeptCode(updateReqVO.getParentId());
|
||||
validateDeptCodeUnique(updateReqVO.getId(), newCode);
|
||||
}
|
||||
updateReqVO.setCode(newCode);
|
||||
} else {
|
||||
updateReqVO.setCode(originalDept.getCode());
|
||||
if (Objects.equals(newParentId, DeptDO.PARENT_ID_ROOT)) {
|
||||
String requestedCode = updateReqVO.getCode();
|
||||
if (StrUtil.isNotBlank(requestedCode) && !StrUtil.equals(requestedCode.trim(), originalDept.getCode())) {
|
||||
updateReqVO.setCode(resolveTopLevelCode(updateReqVO.getId(), requestedCode));
|
||||
} else {
|
||||
updateReqVO.setCode(originalDept.getCode());
|
||||
}
|
||||
} else {
|
||||
updateReqVO.setCode(originalDept.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
// 更新部门
|
||||
@@ -189,7 +211,11 @@ public class DeptServiceImpl implements DeptService {
|
||||
|
||||
@VisibleForTesting
|
||||
void validateDeptNameUnique(Long id, Long parentId, String name) {
|
||||
DeptDO dept = deptMapper.selectByParentIdAndName(parentId, name);
|
||||
Long effectiveParentId = normalizeParentId(parentId);
|
||||
if (Objects.equals(effectiveParentId, DeptDO.PARENT_ID_ROOT)) {
|
||||
return;
|
||||
}
|
||||
DeptDO dept = deptMapper.selectByParentIdAndName(effectiveParentId, name);
|
||||
if (dept == null) {
|
||||
return;
|
||||
}
|
||||
@@ -312,6 +338,18 @@ public class DeptServiceImpl implements DeptService {
|
||||
deptMapper.updateById(update);
|
||||
}
|
||||
|
||||
private String resolveTopLevelCode(Long currentDeptId, String requestedCode) {
|
||||
String candidate = requestedCode;
|
||||
if (candidate != null) {
|
||||
candidate = candidate.trim();
|
||||
}
|
||||
if (StrUtil.isBlank(candidate)) {
|
||||
candidate = generateDeptCode(DeptDO.PARENT_ID_ROOT);
|
||||
}
|
||||
validateDeptCodeUnique(currentDeptId, candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeptDO getDept(Long id) {
|
||||
return deptMapper.selectById(id);
|
||||
|
||||
@@ -84,6 +84,45 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
|
||||
assertEquals(parentDept.getCode() + "001", childDept.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateDept_topLevelAutoCodeAndDuplicates() {
|
||||
DeptSaveReqVO topLevelReq = new DeptSaveReqVO();
|
||||
topLevelReq.setParentId(DeptDO.PARENT_ID_ROOT);
|
||||
topLevelReq.setName("总部");
|
||||
topLevelReq.setSort(1);
|
||||
topLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
topLevelReq.setDeptSource(1);
|
||||
Long topLevelId = deptService.createDept(topLevelReq);
|
||||
DeptDO firstTop = deptMapper.selectById(topLevelId);
|
||||
assertEquals("ZT001", firstTop.getCode());
|
||||
|
||||
DeptSaveReqVO secondTopLevelReq = new DeptSaveReqVO();
|
||||
secondTopLevelReq.setParentId(DeptDO.PARENT_ID_ROOT);
|
||||
secondTopLevelReq.setName("总部");
|
||||
secondTopLevelReq.setSort(2);
|
||||
secondTopLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
secondTopLevelReq.setDeptSource(1);
|
||||
Long secondTopId = deptService.createDept(secondTopLevelReq);
|
||||
DeptDO secondTop = deptMapper.selectById(secondTopId);
|
||||
assertEquals("ZT002", secondTop.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateDept_topLevelRespectCustomCode() {
|
||||
String customCode = "ROOT-001";
|
||||
DeptSaveReqVO topLevelReq = new DeptSaveReqVO();
|
||||
topLevelReq.setParentId(DeptDO.PARENT_ID_ROOT);
|
||||
topLevelReq.setName("集团");
|
||||
topLevelReq.setSort(1);
|
||||
topLevelReq.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
topLevelReq.setDeptSource(1);
|
||||
topLevelReq.setCode(customCode);
|
||||
|
||||
Long deptId = deptService.createDept(topLevelReq);
|
||||
DeptDO created = deptMapper.selectById(deptId);
|
||||
assertEquals(customCode, created.getCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateDept() {
|
||||
// mock 数据
|
||||
@@ -205,20 +244,68 @@ public class DeptServiceImplTest extends BaseDbUnitTest {
|
||||
|
||||
@Test
|
||||
public void testValidateNameUnique_duplicate() {
|
||||
// mock 数据
|
||||
DeptDO deptDO = randomPojo(DeptDO.class).setDeptSource(null);
|
||||
// mock 上级部门
|
||||
DeptDO parentDept = randomPojo(DeptDO.class, o -> {
|
||||
o.setParentId(DeptDO.PARENT_ID_ROOT);
|
||||
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}).setDeptSource(null);
|
||||
deptMapper.insert(parentDept);
|
||||
|
||||
// mock 同级重名部门
|
||||
String duplicateName = randomString(6);
|
||||
DeptDO deptDO = randomPojo(DeptDO.class, o -> {
|
||||
o.setParentId(parentDept.getId());
|
||||
o.setName(duplicateName);
|
||||
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}).setDeptSource(null);
|
||||
deptMapper.insert(deptDO);
|
||||
|
||||
// 准备参数
|
||||
Long id = randomLongId();
|
||||
Long parentId = deptDO.getParentId();
|
||||
String name = deptDO.getName();
|
||||
|
||||
// 调用, 并断言异常
|
||||
assertServiceException(() -> deptService.validateDeptNameUnique(id, parentId, name),
|
||||
assertServiceException(() -> deptService.validateDeptNameUnique(randomLongId(), parentDept.getId(), duplicateName),
|
||||
DEPT_NAME_DUPLICATE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateDeptNameUnique_topLevelDuplicateAllowed() {
|
||||
// mock 顶级部门
|
||||
String duplicateName = randomString(6);
|
||||
DeptDO topLevelDept = randomPojo(DeptDO.class, o -> {
|
||||
o.setParentId(DeptDO.PARENT_ID_ROOT);
|
||||
o.setName(duplicateName);
|
||||
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}).setDeptSource(null);
|
||||
deptMapper.insert(topLevelDept);
|
||||
|
||||
// 调用, 验证不会抛出异常
|
||||
assertDoesNotThrow(() -> deptService.validateDeptNameUnique(null, DeptDO.PARENT_ID_ROOT, duplicateName));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateDeptNameUnique_differentParentAllowed() {
|
||||
// mock 不同上级部门
|
||||
DeptDO parentA = randomPojo(DeptDO.class, o -> {
|
||||
o.setParentId(DeptDO.PARENT_ID_ROOT);
|
||||
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}).setDeptSource(null);
|
||||
deptMapper.insert(parentA);
|
||||
DeptDO parentB = randomPojo(DeptDO.class, o -> {
|
||||
o.setParentId(DeptDO.PARENT_ID_ROOT);
|
||||
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}).setDeptSource(null);
|
||||
deptMapper.insert(parentB);
|
||||
|
||||
String duplicateName = randomString(6);
|
||||
DeptDO childUnderA = randomPojo(DeptDO.class, o -> {
|
||||
o.setParentId(parentA.getId());
|
||||
o.setName(duplicateName);
|
||||
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}).setDeptSource(null);
|
||||
deptMapper.insert(childUnderA);
|
||||
|
||||
// 调用, 验证不同父级可以重名
|
||||
assertDoesNotThrow(() -> deptService.validateDeptNameUnique(null, parentB.getId(), duplicateName));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDept() {
|
||||
// mock 数据
|
||||
|
||||
Reference in New Issue
Block a user