feat: 处理整数溢出并持久化无法处理的数据
- 更新 heartbeatProcessor.js 以处理所有数字溢出类型(int16、int32、int64)并使用二进制补码。 - 防止仅与数据相关的 PostgreSQL 失败抛出个别回退错误。 - 在 databaseManager.js 中添加 insertHeartbeatEventsErrors 方法以存储被拒绝的记录。 - 更新 heartbeatProcessor.js 中的 _emitRejectedRecord 方法,直接将所有无法处理的心跳数据写入 heartbeat_events_errors 数据库。 - 更新 openspec 规范以支持新的溢出和验证回退状态。 - 添加测试文件以验证大整数处理。
This commit is contained in:
@@ -22,18 +22,18 @@ class DatabaseManager {
|
||||
idleTimeoutMillis: this.config.idleTimeoutMillis,
|
||||
connectionTimeoutMillis: 5000, // 5秒连接超时,防止断网时无限等待
|
||||
});
|
||||
|
||||
|
||||
// 监听连接池错误,防止后端断开导致进程崩溃
|
||||
this.pool.on('error', (err, client) => {
|
||||
console.error('[db] 发生未捕获的连接池错误:', err);
|
||||
// 不抛出,让应用层通过心跳检测发现问题
|
||||
});
|
||||
|
||||
|
||||
// 测试连接
|
||||
const client = await this.pool.connect();
|
||||
client.release();
|
||||
console.log('数据库连接池创建成功');
|
||||
|
||||
|
||||
// 初始化表结构
|
||||
await this.initTables();
|
||||
|
||||
@@ -570,15 +570,55 @@ class DatabaseManager {
|
||||
await this.pool.query(singleSql, toRowValues(event));
|
||||
insertedCount += 1;
|
||||
} catch (error) {
|
||||
const connCodes = ['57P03', '08006', '08001', '08003', '08004', '08007'];
|
||||
if (connCodes.includes(error?.code) || (error?.message && /ECONNREFUSED|ETIMEDOUT|connection/i.test(error.message))) {
|
||||
throw error;
|
||||
}
|
||||
failedRecords.push({ error, record: event });
|
||||
}
|
||||
}
|
||||
|
||||
if (insertedCount === 0 && failedRecords.length === events.length) {
|
||||
throw lastError;
|
||||
// 只有在确定是纯网络断开时(所有单独重试都触发同一级别的连接错误),才向外抛出网络错误以重试批次。
|
||||
const connCodes = ['57P03', '08006', '08001', '08003', '08004', '08007'];
|
||||
const isConnError = connCodes.includes(lastError?.code) ||
|
||||
(lastError?.message && /ECONNREFUSED|ETIMEDOUT|connection/i.test(lastError.message));
|
||||
if (isConnError) {
|
||||
throw lastError;
|
||||
}
|
||||
}
|
||||
|
||||
return { insertedCount, failedRecords, batchError: lastError };
|
||||
return { insertedCount, failedRecords, batchError: (insertedCount === 0 && failedRecords.length === events.length) ? lastError : null };
|
||||
}
|
||||
|
||||
async insertHeartbeatEventsErrors(errorsList) {
|
||||
if (!errorsList || errorsList.length === 0) return;
|
||||
try {
|
||||
const placeholders = [];
|
||||
const values = [];
|
||||
let i = 1;
|
||||
for (const err of errorsList) {
|
||||
let hotel_id = Number(err.hotel_id);
|
||||
if (!Number.isFinite(hotel_id)) hotel_id = null;
|
||||
|
||||
let room_id = Number(err.room_id);
|
||||
if (!Number.isFinite(room_id)) room_id = null;
|
||||
|
||||
let original_data = null;
|
||||
try { original_data = JSON.stringify(err.original_data); } catch { /* ignore */ }
|
||||
|
||||
const error_code = String(err.error_code || '');
|
||||
const error_message = String(err.error_message || '');
|
||||
|
||||
placeholders.push(`($${i}, $${i + 1}, $${i + 2}, $${i + 3}, $${i + 4})`);
|
||||
values.push(hotel_id, room_id, original_data, error_code, error_message);
|
||||
i += 5;
|
||||
}
|
||||
const sql = `INSERT INTO heartbeat.heartbeat_events_errors (hotel_id, room_id, original_data, error_code, error_message) VALUES ${placeholders.join(', ')}`;
|
||||
await this.pool.query(sql, values);
|
||||
} catch (e) {
|
||||
console.warn('Failed to insert into heartbeat_events_errors:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async insertHeartbeatData(data) {
|
||||
@@ -586,11 +626,11 @@ class DatabaseManager {
|
||||
if (!Array.isArray(data)) {
|
||||
data = [data];
|
||||
}
|
||||
|
||||
|
||||
if (data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 构建批量插入语句
|
||||
const values = data.map(item => [
|
||||
item.component_id,
|
||||
@@ -598,7 +638,7 @@ class DatabaseManager {
|
||||
item.timestamp,
|
||||
item.data
|
||||
]);
|
||||
|
||||
|
||||
const query = {
|
||||
text: `
|
||||
INSERT INTO heartbeat (component_id, status, timestamp, data)
|
||||
@@ -606,7 +646,7 @@ class DatabaseManager {
|
||||
`,
|
||||
values: values.flat()
|
||||
};
|
||||
|
||||
|
||||
const res = await this.pool.query(query);
|
||||
console.log(`成功插入 ${data.length} 条心跳数据`);
|
||||
return { insertedCount: Number(res?.rowCount ?? data.length) };
|
||||
@@ -706,11 +746,11 @@ class DatabaseManager {
|
||||
// 使用 IS DISTINCT FROM 避免无意义更新
|
||||
const updateColumns = columns.filter(c => !['hotel_id', 'room_id', 'device_id'].includes(c));
|
||||
const updateSet = updateColumns.map(col => `${col} = EXCLUDED.${col}`).join(', ');
|
||||
|
||||
|
||||
// 构建 WHERE 子句:仅当至少一个字段发生变化,且时间戳未回退时才更新
|
||||
// 注意:room_status.room_status_moment.ts_ms 是 bigint,EXCLUDED.ts_ms 也是 bigint
|
||||
const whereConditions = updateColumns.map(col => `room_status.room_status_moment.${col} IS DISTINCT FROM EXCLUDED.${col}`).join(' OR ');
|
||||
|
||||
|
||||
// 生成批量插入 SQL
|
||||
// 注意:ON CONFLICT (hotel_id, room_id, device_id) 依赖于唯一索引 idx_room_status_unique_device
|
||||
const values = [];
|
||||
@@ -753,7 +793,7 @@ class DatabaseManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 不抛出错误,只记录日志,避免影响主流程(Heartbeat History 写入已成功)
|
||||
console.warn('[db] upsertRoomStatus failed:', error.message);
|
||||
return { error };
|
||||
@@ -804,7 +844,7 @@ class DatabaseManager {
|
||||
`,
|
||||
values: [componentId]
|
||||
};
|
||||
|
||||
|
||||
const result = await this.pool.query(query);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
@@ -824,7 +864,7 @@ class DatabaseManager {
|
||||
`,
|
||||
values: [componentId, startTime, endTime]
|
||||
};
|
||||
|
||||
|
||||
const result = await this.pool.query(query);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user