perf: 收敛工作流状态与高 IO 节点开销
- 落地 Redis 版本状态、触发租约和定义缓存 - 优化数据批写、插件请求、文件下载与审计日志 - 补齐循环范围校验、轮询兼容和专项测试
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user