fix(FEAT-DS-001): 修正MySQL健康检查会话时区
This commit is contained in:
@@ -24,23 +24,32 @@ describe('基础设施只读健康客户端', () => {
|
||||
});
|
||||
|
||||
it('MySQL 使用固定元数据查询并返回实际 5.7.x 版本', async () => {
|
||||
const queries: string[] = [];
|
||||
const operations: string[] = [];
|
||||
const client = new MysqlHealthClient(
|
||||
{
|
||||
query: async (sql: string) => {
|
||||
queries.push(sql);
|
||||
return [
|
||||
[
|
||||
{
|
||||
version: '5.7.41-log',
|
||||
databaseName: 'a_share_analysis_dev',
|
||||
charset: 'utf8mb4',
|
||||
collation: 'utf8mb4_general_ci',
|
||||
timezone: '+08:00',
|
||||
},
|
||||
],
|
||||
[],
|
||||
];
|
||||
getConnection: async () => {
|
||||
operations.push('GET_CONNECTION');
|
||||
return {
|
||||
query: async (sql: string) => {
|
||||
operations.push(sql);
|
||||
if (sql === "SET SESSION time_zone = '+08:00'") return [[], []];
|
||||
return [
|
||||
[
|
||||
{
|
||||
version: '5.7.41-log',
|
||||
databaseName: 'a_share_analysis_dev',
|
||||
charset: 'utf8mb4',
|
||||
collation: 'utf8mb4_general_ci',
|
||||
timezone: '+08:00',
|
||||
},
|
||||
],
|
||||
[],
|
||||
];
|
||||
},
|
||||
release: () => {
|
||||
operations.push('RELEASE');
|
||||
},
|
||||
};
|
||||
},
|
||||
end: async () => undefined,
|
||||
},
|
||||
@@ -51,26 +60,60 @@ describe('基础设施只读健康客户端', () => {
|
||||
status: 'healthy',
|
||||
detail: 'MySQL 5.7.41 / a_share_analysis_dev / +08:00',
|
||||
});
|
||||
expect(queries).toEqual([MYSQL_HEALTH_QUERY]);
|
||||
expect(operations).toEqual([
|
||||
'GET_CONNECTION',
|
||||
"SET SESSION time_zone = '+08:00'",
|
||||
MYSQL_HEALTH_QUERY,
|
||||
'RELEASE',
|
||||
]);
|
||||
});
|
||||
|
||||
it('MySQL 查询失败时仍释放当前连接', async () => {
|
||||
const operations: string[] = [];
|
||||
const client = new MysqlHealthClient(
|
||||
{
|
||||
getConnection: async () => ({
|
||||
query: async (sql: string) => {
|
||||
operations.push(sql);
|
||||
if (sql === MYSQL_HEALTH_QUERY) throw new Error('query failed');
|
||||
return [[], []];
|
||||
},
|
||||
release: () => {
|
||||
operations.push('RELEASE');
|
||||
},
|
||||
}),
|
||||
end: async () => undefined,
|
||||
},
|
||||
'a_share_analysis_dev',
|
||||
);
|
||||
|
||||
await expect(client.check()).rejects.toThrow('query failed');
|
||||
expect(operations).toEqual(["SET SESSION time_zone = '+08:00'", MYSQL_HEALTH_QUERY, 'RELEASE']);
|
||||
});
|
||||
|
||||
it('MySQL 拒绝版本、库名或连接口径漂移', async () => {
|
||||
const createClient = (override: Record<string, string>) =>
|
||||
new MysqlHealthClient(
|
||||
{
|
||||
query: async () => [
|
||||
[
|
||||
{
|
||||
version: '5.7.42',
|
||||
databaseName: 'a_share_analysis_dev',
|
||||
charset: 'utf8mb4',
|
||||
collation: 'utf8mb4_general_ci',
|
||||
timezone: '+08:00',
|
||||
...override,
|
||||
},
|
||||
],
|
||||
[],
|
||||
],
|
||||
getConnection: async () => ({
|
||||
query: async (sql: string) =>
|
||||
sql === MYSQL_HEALTH_QUERY
|
||||
? [
|
||||
[
|
||||
{
|
||||
version: '5.7.42',
|
||||
databaseName: 'a_share_analysis_dev',
|
||||
charset: 'utf8mb4',
|
||||
collation: 'utf8mb4_general_ci',
|
||||
timezone: '+08:00',
|
||||
...override,
|
||||
},
|
||||
],
|
||||
[],
|
||||
]
|
||||
: [[], []],
|
||||
release: () => undefined,
|
||||
}),
|
||||
end: async () => undefined,
|
||||
},
|
||||
'a_share_analysis_dev',
|
||||
@@ -92,18 +135,24 @@ describe('基础设施只读健康客户端', () => {
|
||||
it('MySQL 按当前配置校验生产库名', async () => {
|
||||
const client = new MysqlHealthClient(
|
||||
{
|
||||
query: async () => [
|
||||
[
|
||||
{
|
||||
version: '5.7.42',
|
||||
databaseName: 'a_share_analysis',
|
||||
charset: 'utf8mb4',
|
||||
collation: 'utf8mb4_general_ci',
|
||||
timezone: '+08:00',
|
||||
},
|
||||
],
|
||||
[],
|
||||
],
|
||||
getConnection: async () => ({
|
||||
query: async (sql: string) =>
|
||||
sql === MYSQL_HEALTH_QUERY
|
||||
? [
|
||||
[
|
||||
{
|
||||
version: '5.7.42',
|
||||
databaseName: 'a_share_analysis',
|
||||
charset: 'utf8mb4',
|
||||
collation: 'utf8mb4_general_ci',
|
||||
timezone: '+08:00',
|
||||
},
|
||||
],
|
||||
[],
|
||||
]
|
||||
: [[], []],
|
||||
release: () => undefined,
|
||||
}),
|
||||
end: async () => undefined,
|
||||
},
|
||||
'a_share_analysis',
|
||||
|
||||
@@ -12,8 +12,16 @@ export const MYSQL_HEALTH_QUERY = `SELECT
|
||||
@@collation_database AS collation,
|
||||
@@session.time_zone AS timezone`;
|
||||
|
||||
interface MysqlQueryPort {
|
||||
/** mysql2 的 timezone 选项只控制日期转换,因此需显式设置服务端会话时区。 */
|
||||
export const MYSQL_SESSION_TIMEZONE_QUERY = "SET SESSION time_zone = '+08:00'";
|
||||
|
||||
interface MysqlConnectionPort {
|
||||
query(sql: string): Promise<[unknown, unknown]>;
|
||||
release(): void;
|
||||
}
|
||||
|
||||
interface MysqlPoolPort {
|
||||
getConnection(): Promise<MysqlConnectionPort>;
|
||||
end(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -28,31 +36,37 @@ interface MysqlMetadataRow {
|
||||
/** MySQL 健康客户端只校验元数据,不读取表数据或执行写入。 */
|
||||
export class MysqlHealthClient implements OnModuleDestroy {
|
||||
constructor(
|
||||
private readonly client: MysqlQueryPort,
|
||||
private readonly client: MysqlPoolPort,
|
||||
private readonly expectedDatabase: string,
|
||||
) {}
|
||||
|
||||
async check(): Promise<DependencyHealthDto> {
|
||||
const [rows] = await this.client.query(MYSQL_HEALTH_QUERY);
|
||||
const row = (rows as unknown as MysqlMetadataRow[])[0];
|
||||
if (!row) throw new Error('MySQL 未返回元数据');
|
||||
const versionMatch = /^(5\.7\.\d+)(?:[-+].*)?$/.exec(row.version ?? '');
|
||||
if (!versionMatch) {
|
||||
throw new Error('MySQL 5.7.x 版本要求未满足');
|
||||
}
|
||||
if (row.databaseName !== this.expectedDatabase) {
|
||||
throw new Error(`MySQL 健康检查必须连接 ${this.expectedDatabase}`);
|
||||
}
|
||||
if (row.charset !== 'utf8mb4') throw new Error('MySQL 字符集必须为 utf8mb4');
|
||||
if (row.collation !== 'utf8mb4_general_ci') {
|
||||
throw new Error('MySQL 排序规则必须为 utf8mb4_general_ci');
|
||||
}
|
||||
if (row.timezone !== '+08:00') throw new Error('MySQL 会话时区必须为 +08:00');
|
||||
const connection = await this.client.getConnection();
|
||||
try {
|
||||
await connection.query(MYSQL_SESSION_TIMEZONE_QUERY);
|
||||
const [rows] = await connection.query(MYSQL_HEALTH_QUERY);
|
||||
const row = (rows as unknown as MysqlMetadataRow[])[0];
|
||||
if (!row) throw new Error('MySQL 未返回元数据');
|
||||
const versionMatch = /^(5\.7\.\d+)(?:[-+].*)?$/.exec(row.version ?? '');
|
||||
if (!versionMatch) {
|
||||
throw new Error('MySQL 5.7.x 版本要求未满足');
|
||||
}
|
||||
if (row.databaseName !== this.expectedDatabase) {
|
||||
throw new Error(`MySQL 健康检查必须连接 ${this.expectedDatabase}`);
|
||||
}
|
||||
if (row.charset !== 'utf8mb4') throw new Error('MySQL 字符集必须为 utf8mb4');
|
||||
if (row.collation !== 'utf8mb4_general_ci') {
|
||||
throw new Error('MySQL 排序规则必须为 utf8mb4_general_ci');
|
||||
}
|
||||
if (row.timezone !== '+08:00') throw new Error('MySQL 会话时区必须为 +08:00');
|
||||
|
||||
return {
|
||||
status: 'healthy',
|
||||
detail: `MySQL ${versionMatch[1]} / ${row.databaseName} / ${row.timezone}`,
|
||||
};
|
||||
return {
|
||||
status: 'healthy',
|
||||
detail: `MySQL ${versionMatch[1]} / ${row.databaseName} / ${row.timezone}`,
|
||||
};
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user