feat: 添加 G5 room_status 写入的冲突键排序与去重功能,并实现死锁自动重试机制
This commit is contained in:
@@ -130,6 +130,14 @@ class DatabaseManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
_isDeadlockError(error) {
|
||||
return String(error?.code ?? '') === '40P01';
|
||||
}
|
||||
|
||||
async _sleep(ms) {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// ---- 旧表列定义 ----
|
||||
|
||||
_getLegacyColumns() {
|
||||
@@ -785,7 +793,7 @@ class DatabaseManager {
|
||||
}
|
||||
|
||||
_buildRoomStatusUpsertQuery(events, target) {
|
||||
const { tableName, conflictColumns, includeGuid, tableRef, forceOnlineStatusOnWrite, forceUpdateOnConflict } = target;
|
||||
const { tableName, conflictColumns, includeGuid, tableRef, forceOnlineStatusOnWrite, forceUpdateOnConflict, sortAndDedupByConflictKey } = target;
|
||||
const columns = this._getRoomStatusBaseColumns();
|
||||
const uniqueEventsMap = new Map();
|
||||
|
||||
@@ -799,7 +807,22 @@ class DatabaseManager {
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueEvents = Array.from(uniqueEventsMap.values());
|
||||
let uniqueEvents = Array.from(uniqueEventsMap.values());
|
||||
if (sortAndDedupByConflictKey) {
|
||||
uniqueEvents = uniqueEvents.sort((a, b) => {
|
||||
for (const column of conflictColumns) {
|
||||
const av = String(a?.[column] ?? '');
|
||||
const bv = String(b?.[column] ?? '');
|
||||
const cmp = av.localeCompare(bv, 'zh-CN', { numeric: true });
|
||||
if (cmp !== 0) return cmp;
|
||||
}
|
||||
const ats = BigInt(a?.ts_ms ?? 0);
|
||||
const bts = BigInt(b?.ts_ms ?? 0);
|
||||
if (ats === bts) return 0;
|
||||
return ats < bts ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
if (uniqueEvents.length === 0) {
|
||||
return { sql: null, values: [], uniqueEvents: [] };
|
||||
}
|
||||
@@ -855,28 +878,44 @@ class DatabaseManager {
|
||||
const { sql, values, uniqueEvents } = this._buildRoomStatusUpsertQuery(events, target);
|
||||
if (!sql || uniqueEvents.length === 0) return { rowCount: 0 };
|
||||
|
||||
try {
|
||||
const res = await this.pool.query(sql, values);
|
||||
return { rowCount: res.rowCount };
|
||||
} catch (error) {
|
||||
if (target.autoCreatePartitions && this.isRoomStatusMissingPartitionError(error)) {
|
||||
const hotelIds = [...new Set(uniqueEvents.map((event) => event.hotel_id).filter((id) => id != null))];
|
||||
if (hotelIds.length > 0) {
|
||||
console.log(`[db] 检测到 room_status 分区缺失,尝试自动创建分区,hotelIds: ${hotelIds.join(', ')}`);
|
||||
await this.ensureRoomStatusPartitions(hotelIds);
|
||||
try {
|
||||
const res = await this.pool.query(sql, values);
|
||||
return { rowCount: res.rowCount };
|
||||
} catch (retryError) {
|
||||
console.warn(`[db] ${target.logPrefix} retry failed:`, retryError.message);
|
||||
return { error: retryError };
|
||||
const deadlockRetryAttempts = Number(target.deadlockRetryAttempts ?? 0);
|
||||
const deadlockRetryBaseDelayMs = Number(target.deadlockRetryBaseDelayMs ?? 100);
|
||||
|
||||
for (let attempt = 0; attempt <= deadlockRetryAttempts; attempt += 1) {
|
||||
try {
|
||||
const res = await this.pool.query(sql, values);
|
||||
return { rowCount: res.rowCount };
|
||||
} catch (error) {
|
||||
if (target.autoCreatePartitions && this.isRoomStatusMissingPartitionError(error)) {
|
||||
const hotelIds = [...new Set(uniqueEvents.map((event) => event.hotel_id).filter((id) => id != null))];
|
||||
if (hotelIds.length > 0) {
|
||||
console.log(`[db] 检测到 room_status 分区缺失,尝试自动创建分区,hotelIds: ${hotelIds.join(', ')}`);
|
||||
await this.ensureRoomStatusPartitions(hotelIds);
|
||||
try {
|
||||
const res = await this.pool.query(sql, values);
|
||||
return { rowCount: res.rowCount };
|
||||
} catch (retryError) {
|
||||
console.warn(`[db] ${target.logPrefix} retry failed:`, retryError.message);
|
||||
return { error: retryError };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.warn(`[db] ${target.logPrefix} failed:`, error.message);
|
||||
return { error };
|
||||
const shouldRetryDeadlock = this._isDeadlockError(error) && attempt < deadlockRetryAttempts;
|
||||
if (shouldRetryDeadlock) {
|
||||
const jitter = Math.floor(Math.random() * Math.max(1, deadlockRetryBaseDelayMs));
|
||||
const delay = deadlockRetryBaseDelayMs * (attempt + 1) + jitter;
|
||||
console.warn(`[db] ${target.logPrefix} deadlock detected, retrying in ${delay}ms (attempt ${attempt + 1}/${deadlockRetryAttempts})`);
|
||||
await this._sleep(delay);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.warn(`[db] ${target.logPrefix} failed:`, error.message);
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
|
||||
return { error: new Error(`${target.logPrefix} failed after deadlock retries`) };
|
||||
}
|
||||
|
||||
|
||||
@@ -892,6 +931,9 @@ class DatabaseManager {
|
||||
tableRef: this.config.roomStatusTable ?? 'room_status.room_status_moment',
|
||||
forceOnlineStatusOnWrite: false,
|
||||
forceUpdateOnConflict: false,
|
||||
sortAndDedupByConflictKey: false,
|
||||
deadlockRetryAttempts: 0,
|
||||
deadlockRetryBaseDelayMs: 100,
|
||||
logPrefix: 'upsertRoomStatus',
|
||||
});
|
||||
}
|
||||
@@ -905,6 +947,9 @@ class DatabaseManager {
|
||||
tableRef: this.config.roomStatusTable ?? 'room_status.room_status_moment_g5',
|
||||
forceOnlineStatusOnWrite: true,
|
||||
forceUpdateOnConflict: true,
|
||||
sortAndDedupByConflictKey: true,
|
||||
deadlockRetryAttempts: 3,
|
||||
deadlockRetryBaseDelayMs: 100,
|
||||
logPrefix: 'upsertRoomStatusG5',
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user