perf: 收敛工作流状态与高 IO 节点开销

- 落地 Redis 版本状态、触发租约和定义缓存

- 优化数据批写、插件请求、文件下载与审计日志

- 补齐循环范围校验、轮询兼容和专项测试
This commit is contained in:
2026-07-29 00:47:48 +08:00
parent 5ee6065017
commit 1ae8a22afe
103 changed files with 15600 additions and 532 deletions

View File

@@ -0,0 +1,341 @@
package tech.easyflow.datacenter.connector.impl;
import com.alibaba.fastjson2.JSONObject;
import org.junit.Test;
import tech.easyflow.datacenter.entity.DatacenterTable;
import tech.easyflow.datacenter.entity.DatacenterTableField;
import tech.easyflow.datacenter.meta.entity.DatacenterSource;
import tech.easyflow.common.web.exceptions.BusinessException;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 项目 MySQL 连接器批量写入测试。
*/
public class ProjectMysqlConnectorBatchTest {
/**
* 验证原始 SQL 在单连接、单 ResultSet 中原样流式消费。
*
* @throws Exception JDBC 模拟初始化失败时抛出
*/
@Test
public void shouldStreamOriginalSqlWithoutPaginationRewrite()
throws Exception {
DataSource dataSource = mock(DataSource.class);
Connection connection = mock(Connection.class);
PreparedStatement statement =
mock(PreparedStatement.class);
ResultSet resultSet = mock(ResultSet.class);
ResultSetMetaData metadata =
mock(ResultSetMetaData.class);
String sql =
"SELECT id FROM sample LIMIT 10 FOR UPDATE";
when(dataSource.getConnection())
.thenReturn(connection);
when(connection.prepareStatement(
eq(sql),
eq(ResultSet.TYPE_FORWARD_ONLY),
eq(ResultSet.CONCUR_READ_ONLY)))
.thenReturn(statement);
when(statement.executeQuery())
.thenReturn(resultSet);
when(resultSet.getMetaData())
.thenReturn(metadata);
when(metadata.getColumnCount()).thenReturn(1);
when(metadata.getColumnLabel(1)).thenReturn("id");
when(resultSet.next())
.thenReturn(true, false);
when(resultSet.getObject(1))
.thenReturn(1L);
List<String> ids = new ArrayList<>();
new ProjectMysqlConnector(dataSource)
.consumeBySql(
source(),
sql,
1_000,
row -> ids.add(
row.getString("id")));
org.junit.Assert.assertEquals(
List.of("1"), ids);
verify(dataSource).getConnection();
verify(connection).prepareStatement(
sql,
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY);
verify(statement).setFetchSize(
Integer.MIN_VALUE);
verify(statement).executeQuery();
}
/**
* 验证多行写入仅获取一次连接,并按批次执行 JDBC batch。
*
* @throws Exception JDBC 模拟初始化失败时抛出
*/
@Test
public void shouldReuseSingleConnectionAndExecuteConfiguredBatches() throws Exception {
DataSource dataSource = mock(DataSource.class);
Connection connection = mock(Connection.class);
List<PreparedStatement> statements = new ArrayList<>();
when(dataSource.getConnection()).thenReturn(connection);
when(connection.prepareStatement(anyString())).thenAnswer(invocation -> {
PreparedStatement statement = mock(PreparedStatement.class);
statements.add(statement);
return statement;
});
ProjectMysqlConnector connector = new ProjectMysqlConnector(dataSource);
DatacenterSource source = new DatacenterSource();
source.setDatabaseName("easyflow");
DatacenterTable table = new DatacenterTable();
table.setTableName("sample");
DatacenterTableField nameField = new DatacenterTableField();
nameField.setFieldName("name");
nameField.setWritable(1);
table.setFields(List.of(nameField));
List<JSONObject> rows = new ArrayList<>();
for (int index = 0; index < 5; index++) {
JSONObject row = new JSONObject();
row.put("name", "row-" + index);
rows.add(row);
}
connector.saveRows(source, table, rows, null, 2);
verify(dataSource, times(1)).getConnection();
if (statements.size() != 3) {
throw new AssertionError("expected 3 JDBC batches but got " + statements.size());
}
int addBatchCalls = 0;
for (PreparedStatement statement : statements) {
verify(statement, times(1)).executeBatch();
addBatchCalls += org.mockito.Mockito.mockingDetails(statement)
.getInvocations()
.stream()
.filter(invocation -> "addBatch".equals(invocation.getMethod().getName()))
.count();
}
if (addBatchCalls != rows.size()) {
throw new AssertionError("expected " + rows.size() + " addBatch calls but got " + addBatchCalls);
}
}
/**
* 验证回执和业务批量写入在同一 JDBC 事务中提交。
*
* @throws Exception JDBC 模拟初始化失败时抛出
*/
@Test
public void shouldCommitReceiptAndRowsInSingleTransaction() throws Exception {
DataSource dataSource = mock(DataSource.class);
Connection connection = mock(Connection.class);
PreparedStatement receiptStatement = mock(PreparedStatement.class);
PreparedStatement queryStatement = mock(PreparedStatement.class);
PreparedStatement rowStatement = mock(PreparedStatement.class);
ResultSet resultSet = mock(ResultSet.class);
when(dataSource.getConnection()).thenReturn(connection);
when(connection.getAutoCommit()).thenReturn(true);
when(connection.prepareStatement(anyString())).thenAnswer(invocation -> {
String sql = invocation.getArgument(0);
if (sql.startsWith("SELECT")) {
return queryStatement;
}
return sql.contains("tb_datacenter_write_receipt")
? receiptStatement
: rowStatement;
});
when(queryStatement.executeQuery()).thenReturn(resultSet);
when(resultSet.next()).thenReturn(false);
ProjectMysqlConnector connector = new ProjectMysqlConnector(dataSource);
DatacenterSource source = source();
DatacenterTable table = table();
JSONObject row = new JSONObject();
row.put("name", "row-1");
boolean written = connector.saveRowsIdempotently(
source,
table,
List.of(row),
null,
100,
"receipt-key",
"payload-hash");
assertTrue(written);
verify(connection).setAutoCommit(false);
verify(receiptStatement).executeBatch();
verify(receiptStatement).executeUpdate();
verify(rowStatement).executeBatch();
verify(connection, times(2)).commit();
verify(connection, never()).rollback();
verify(connection).setAutoCommit(true);
}
/**
* 验证业务批量失败时回执与业务数据一并回滚。
*
* @throws Exception JDBC 模拟初始化失败时抛出
*/
@Test
public void shouldRollbackReceiptWhenBatchWriteFails() throws Exception {
DataSource dataSource = mock(DataSource.class);
Connection connection = mock(Connection.class);
PreparedStatement receiptStatement = mock(PreparedStatement.class);
PreparedStatement queryStatement = mock(PreparedStatement.class);
PreparedStatement rowStatement = mock(PreparedStatement.class);
ResultSet resultSet = mock(ResultSet.class);
when(dataSource.getConnection()).thenReturn(connection);
when(connection.getAutoCommit()).thenReturn(true);
when(connection.prepareStatement(anyString())).thenAnswer(invocation -> {
String sql = invocation.getArgument(0);
if (sql.startsWith("SELECT")) {
return queryStatement;
}
return sql.contains("tb_datacenter_write_receipt")
? receiptStatement
: rowStatement;
});
when(queryStatement.executeQuery()).thenReturn(resultSet);
when(resultSet.next()).thenReturn(false);
when(rowStatement.executeBatch()).thenThrow(new SQLException("write failed"));
ProjectMysqlConnector connector = new ProjectMysqlConnector(dataSource);
JSONObject row = new JSONObject();
row.put("name", "row-1");
try {
connector.saveRowsIdempotently(
source(),
table(),
List.of(row),
null,
100,
"receipt-key",
"payload-hash");
throw new AssertionError("failed business batch must rollback");
} catch (BusinessException expected) {
assertTrue(expected.getMessage().contains("write failed"));
}
verify(connection, atLeastOnce()).rollback();
verify(connection, never()).commit();
verify(connection).setAutoCommit(true);
}
/**
* 验证中间行失败时前序行已经提交,后续行不会执行。
*
* @throws Exception JDBC 模拟初始化失败时抛出
*/
@Test
public void shouldKeepEarlierRowsCommittedWhenMiddleRowFails() throws Exception {
DataSource dataSource = mock(DataSource.class);
Connection connection = mock(Connection.class);
PreparedStatement receiptStatement = mock(PreparedStatement.class);
PreparedStatement queryStatement = mock(PreparedStatement.class);
PreparedStatement rowStatement = mock(PreparedStatement.class);
ResultSet resultSet = mock(ResultSet.class);
when(dataSource.getConnection()).thenReturn(connection);
when(connection.getAutoCommit()).thenReturn(true);
when(connection.prepareStatement(anyString())).thenAnswer(invocation -> {
String sql = invocation.getArgument(0);
if (sql.startsWith("SELECT")) {
return queryStatement;
}
return sql.contains("tb_datacenter_write_receipt")
? receiptStatement
: rowStatement;
});
when(queryStatement.executeQuery()).thenReturn(resultSet);
when(resultSet.next()).thenReturn(false);
when(rowStatement.executeBatch())
.thenThrow(new SQLException("batch failed"))
.thenReturn(new int[]{1})
.thenThrow(new SQLException("middle row failed"));
ProjectMysqlConnector connector = new ProjectMysqlConnector(dataSource);
List<JSONObject> rows = List.of(
row("row-0"), row("row-1"), row("row-2"));
try {
connector.saveRowsIdempotently(
source(),
table(),
rows,
null,
100,
"receipt-key",
"payload-hash");
throw new AssertionError("middle row failure must be propagated");
} catch (BusinessException expected) {
assertTrue(expected.getMessage().contains("middle row failed"));
}
verify(dataSource, times(1)).getConnection();
verify(connection, times(1)).commit();
verify(connection, atLeastOnce()).rollback();
verify(rowStatement, times(3)).executeBatch();
verify(receiptStatement).executeBatch();
verify(receiptStatement, times(2)).executeUpdate();
}
/**
* 创建测试数据源元数据。
*
* @return 项目 MySQL 数据源
*/
private DatacenterSource source() {
DatacenterSource source = new DatacenterSource();
source.setDatabaseName("easyflow");
return source;
}
/**
* 创建包含一个可写字段的测试表。
*
* @return 测试数据表
*/
private DatacenterTable table() {
DatacenterTable table = new DatacenterTable();
table.setTableName("sample");
DatacenterTableField nameField = new DatacenterTableField();
nameField.setFieldName("name");
nameField.setWritable(1);
table.setFields(List.of(nameField));
return table;
}
/**
* 创建测试数据行。
*
* @param name 行名称
* @return JSON 行
*/
private JSONObject row(String name) {
JSONObject row = new JSONObject();
row.put("name", name);
return row;
}
}

View File

@@ -0,0 +1,113 @@
package tech.easyflow.datacenter.connector.support;
import com.alibaba.fastjson2.JSONObject;
import com.mybatisflex.core.row.Db;
import com.mybatisflex.core.row.Row;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.MockedStatic;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.datacenter.entity.DatacenterTable;
import tech.easyflow.datacenter.entity.DatacenterTableField;
import tech.easyflow.datacenter.meta.entity.DatacenterSource;
import tech.easyflow.datacenter.meta.enums.DatacenterCapability;
import tech.easyflow.datacenter.meta.enums.DatacenterSourceType;
import javax.sql.DataSource;
import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
/**
* 内部动态表幂等批量写入回归测试。
*/
public class AbstractInternalTableConnectorBatchTest {
/**
* 验证正常路径按批次写入回执和数据,不退化为逐行 SQL。
*/
@Test
public void shouldBatchReceiptsAndRowsOnNormalPath() {
DatacenterTable table = mock(DatacenterTable.class);
DatacenterTableField field =
mock(DatacenterTableField.class);
org.mockito.Mockito.when(table.getFields())
.thenReturn(List.of(field));
org.mockito.Mockito.when(table.getMaterializedTable())
.thenReturn("tb_internal_test");
org.mockito.Mockito.when(field.getFieldName())
.thenReturn("name");
LoginAccount account = mock(LoginAccount.class);
org.mockito.Mockito.when(account.getId())
.thenReturn(BigInteger.ONE);
org.mockito.Mockito.when(account.getDeptId())
.thenReturn(BigInteger.ONE);
org.mockito.Mockito.when(account.getTenantId())
.thenReturn(BigInteger.ONE);
JSONObject first = JSONObject.of("name", "first");
JSONObject second = JSONObject.of("name", "second");
try (MockedStatic<Db> db = mockStatic(Db.class)) {
db.when(() -> Db.selectOneByMap(
eq("tb_datacenter_write_receipt"),
anyMap()))
.thenReturn(null);
db.when(() -> Db.txWithResult(
org.mockito.ArgumentMatchers
.<Supplier<Object>>any()))
.thenAnswer(invocation -> invocation
.<Supplier<?>>getArgument(0)
.get());
boolean written = new TestInternalConnector()
.saveRowsIdempotently(
new DatacenterSource(),
table,
List.of(first, second),
account,
2,
"receipt",
"hash");
Assert.assertTrue(written);
db.verify(() -> Db.insertBatch(
eq("tb_datacenter_write_receipt"),
anyCollection(),
eq(2)));
db.verify(() -> Db.insertBatch(
eq("tb_internal_test"),
anyCollection(),
eq(2)));
db.verify(() -> Db.updateBatchById(
eq("tb_internal_test"),
anyList()), never());
}
}
/**
* 仅用于测试内部动态表批量协议的最小连接器。
*/
private static final class TestInternalConnector
extends AbstractInternalTableConnector {
/**
* 创建测试连接器。
*/
private TestInternalConnector() {
super(
DatacenterSourceType.EXCEL,
Collections.<DatacenterCapability>emptySet(),
mock(DataSource.class));
}
}
}

View File

@@ -0,0 +1,125 @@
package tech.easyflow.datacenter.connector.support;
import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.Mockito;
import tech.easyflow.datacenter.connector.dialect.PostgresqlSqlDialect;
import tech.easyflow.datacenter.meta.entity.DatacenterSource;
import tech.easyflow.datacenter.meta.enums.DatacenterCapability;
import tech.easyflow.datacenter.meta.enums.DatacenterSourceType;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.EnumSet;
/**
* PostgreSQL 服务端游标连接状态回归测试。
*/
public class PostgresqlStreamingConnectorTest {
/**
* 验证自动提交连接进入游标事务并在消费完成后恢复。
*
* @throws Exception JDBC 模拟调用失败时抛出
*/
@Test
public void shouldRestoreAutoCommitAfterStreaming()
throws Exception {
Connection connection =
Mockito.mock(Connection.class);
PreparedStatement statement =
Mockito.mock(
PreparedStatement.class);
ResultSet resultSet =
Mockito.mock(ResultSet.class);
ResultSetMetaData metaData =
Mockito.mock(
ResultSetMetaData.class);
Mockito.when(connection.getAutoCommit())
.thenReturn(true);
Mockito.when(connection.prepareStatement(
"SELECT id FROM sample",
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY))
.thenReturn(statement);
Mockito.when(statement.executeQuery())
.thenReturn(resultSet);
Mockito.when(resultSet.getMetaData())
.thenReturn(metaData);
Mockito.when(resultSet.next())
.thenReturn(false);
TestConnector connector =
new TestConnector(connection);
connector.consumeBySql(
new DatacenterSource(),
"SELECT id FROM sample",
512,
row -> {
});
InOrder order = Mockito.inOrder(
connection,
statement,
resultSet);
order.verify(connection)
.getAutoCommit();
order.verify(connection)
.setAutoCommit(false);
order.verify(connection)
.prepareStatement(
"SELECT id FROM sample",
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY);
order.verify(statement)
.setFetchSize(512);
order.verify(statement)
.executeQuery();
order.verify(resultSet)
.close();
order.verify(statement)
.close();
order.verify(connection)
.rollback();
order.verify(connection)
.setAutoCommit(true);
}
/**
* 使用测试连接执行 PostgreSQL 查询。
*/
private static final class TestConnector
extends AbstractJdbcConnector {
private final Connection connection;
/**
* 创建测试连接器。
*
* @param connection 测试 JDBC 连接
*/
private TestConnector(
Connection connection) {
super(
DatacenterSourceType.POSTGRESQL,
new PostgresqlSqlDialect(),
EnumSet.of(
DatacenterCapability.READ_QUERY));
this.connection = connection;
}
/**
* {@inheritDoc}
*/
@Override
protected <T> T withConnection(
DatacenterSource source,
boolean cacheable,
JdbcCallback<T> callback)
throws Exception {
return callback.apply(connection);
}
}
}

View File

@@ -0,0 +1,122 @@
package tech.easyflow.datacenter.execution.service.impl;
import com.alibaba.fastjson2.JSONObject;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import tech.easyflow.common.cache.RedisIdempotencyExecutor;
import tech.easyflow.datacenter.connector.DatacenterConnector;
import tech.easyflow.datacenter.connector.DatacenterConnectorRegistry;
import tech.easyflow.datacenter.entity.DatacenterTable;
import tech.easyflow.datacenter.execution.model.DatasetRef;
import tech.easyflow.datacenter.meta.entity.DatacenterSource;
import tech.easyflow.datacenter.meta.enums.DatacenterSourceType;
import tech.easyflow.datacenter.meta.service.DatacenterDatasetRegistryService;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 数据集写入服务的幂等批写测试。
*/
public class DatacenterDatasetWriteServiceImplTest {
/**
* 验证服务只调用一次连接器,并保留配置的批大小供连接器复用连接处理。
*
* @throws Exception 测试依赖注入失败时抛出
*/
@Test
public void shouldDelegateIdempotentRowsInSingleConnectorCall() throws Exception {
DatacenterDatasetRegistryService registryService =
mock(DatacenterDatasetRegistryService.class);
DatacenterConnectorRegistry connectorRegistry =
mock(DatacenterConnectorRegistry.class);
DatacenterConnector connector = mock(DatacenterConnector.class);
RedisIdempotencyExecutor idempotencyExecutor =
mock(RedisIdempotencyExecutor.class);
BigInteger tableId = BigInteger.ONE;
BigInteger sourceId = BigInteger.TWO;
DatasetRef datasetRef = new DatasetRef();
datasetRef.setTableId(tableId);
DatacenterTable table = new DatacenterTable();
table.setSourceId(sourceId);
DatacenterSource source = new DatacenterSource();
source.setSourceType(DatacenterSourceType.PROJECT_MYSQL.name());
when(registryService.getTableWithFields(tableId)).thenReturn(table);
when(registryService.getSourceRequired(sourceId)).thenReturn(source);
when(connectorRegistry.getConnector(
DatacenterSourceType.PROJECT_MYSQL.name())).thenReturn(connector);
when(idempotencyExecutor.executeOnce(
anyString(), anyString(), any(Runnable.class)))
.thenAnswer(invocation -> {
invocation.<Runnable>getArgument(2).run();
return true;
});
when(connector.saveRowsIdempotently(
any(), any(), anyList(), any(), anyInt(), anyString(), anyString()))
.thenReturn(true);
DatacenterDatasetWriteServiceImpl service =
new DatacenterDatasetWriteServiceImpl();
inject(service, "registryService", registryService);
inject(service, "connectorRegistry", connectorRegistry);
inject(service, "idempotencyExecutor", idempotencyExecutor);
List<JSONObject> rows = List.of(
row("row-0"), row("row-1"), row("row-2"));
Assert.assertTrue(service.saveRowsIdempotently(
datasetRef, rows, null, 64, "stable-execution-key"));
@SuppressWarnings("unchecked")
ArgumentCaptor<List<JSONObject>> rowsCaptor =
ArgumentCaptor.forClass(List.class);
verify(connector, times(1)).saveRowsIdempotently(
eq(source),
eq(table),
rowsCaptor.capture(),
any(),
eq(64),
anyString(),
anyString());
Assert.assertEquals(rows, rowsCaptor.getValue());
}
/**
* 创建测试行。
*
* @param name 行名称
* @return JSON 行
*/
private JSONObject row(String name) {
JSONObject row = new JSONObject();
row.put("name", name);
return row;
}
/**
* 注入服务测试依赖。
*
* @param target 目标服务
* @param fieldName 字段名
* @param value 字段值
* @throws Exception 反射访问失败时抛出
*/
private void inject(Object target, String fieldName, Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
}

View File

@@ -0,0 +1,36 @@
package tech.easyflow.datacenter.schedule;
import org.junit.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 数据集写入回执清理任务测试。
*/
public class DatacenterWriteReceiptCleanupJobTest {
/**
* 验证清理任务按固定大小分批,并在最后一个非满批次后停止。
*/
@Test
public void shouldDeleteExpiredReceiptsInBoundedBatches() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
when(jdbcTemplate.update(anyString(), any(), anyInt()))
.thenReturn(1000, 7);
DatacenterWriteReceiptCleanupJob job =
new DatacenterWriteReceiptCleanupJob(
jdbcTemplate, 14L, 1000, 20);
job.cleanup();
verify(jdbcTemplate, times(2)).update(
anyString(), any(), anyInt());
}
}