fix(sync): 推送异常时落库 failed,修复订正时区与卡住阈值
rsync 抛错时 sync_log 不再永久 running;reconcile 兼容 naive UTC。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
# Changelog — 2026-06-07 推送历史「进行中」订正与异常落库
|
||||
|
||||
## 摘要
|
||||
|
||||
推送失败时 `sync_logs` 长期停留在 `running`(进行中),导致推送历史大量「进行中」。根因:rsync 进程启动异常(如容器缺 `rsync`)在 `_rsync_push` 抛出未捕获异常,终态未写入 DB。
|
||||
|
||||
## 动机
|
||||
|
||||
用户反馈推送历史里很多「进行中」;生产库核实 7/7 条均为 `running`,对应今日多次 rsync 缺失导致的推送尝试。
|
||||
|
||||
## 变更
|
||||
|
||||
- `sync_engine_v2.py`:`_rsync_push` 异常转为 `exit_code=-1`,保证 `sync_log` 落库为 `failed`
|
||||
- `sync_log_repo.py`:订正卡住记录时兼容 MySQL naive UTC 与 aware datetime 相减
|
||||
- `usePushLogs.ts`:「修复卡住记录」阈值 120 分钟 → 15 分钟
|
||||
- 生产数据:7 条历史 `running` 已订正为 `failed`
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `server/application/services/sync_engine_v2.py`
|
||||
- `server/infrastructure/database/sync_log_repo.py`
|
||||
- `frontend/src/composables/push/usePushLogs.ts`
|
||||
|
||||
## 迁移 / 重启
|
||||
|
||||
- 需部署后端 + 前端构建
|
||||
- 无 DB 迁移
|
||||
|
||||
## 验证
|
||||
|
||||
- 推送历史无残留「进行中」(或点「修复卡住记录」可订正 >15 分钟的卡住项)
|
||||
- 模拟 rsync 失败时 `sync_logs.status` 应为 `failed` 而非 `running`
|
||||
@@ -86,7 +86,7 @@ export function usePushLogs(form: PushForm, servers: PushServers) {
|
||||
try {
|
||||
const res = await http.post<{ reconciled: number; max_age_minutes: number }>(
|
||||
'/sync/reconcile-stale-logs',
|
||||
{ max_age_minutes: 120 },
|
||||
{ max_age_minutes: 15 },
|
||||
)
|
||||
const n = res.reconciled ?? 0
|
||||
snackbar(n > 0 ? `已订正 ${n} 条卡住的推送记录` : '没有需要订正的记录')
|
||||
|
||||
@@ -163,7 +163,14 @@ class SyncEngineV2:
|
||||
except Exception as e:
|
||||
logger.debug(f"WS running broadcast failed for server {server.id}: {e}")
|
||||
|
||||
result = await _rsync_push(server, source_path, dest, sync_mode)
|
||||
try:
|
||||
result = await _rsync_push(server, source_path, dest, sync_mode)
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
"rsync push raised for server %s (%s): %s",
|
||||
server.id, server.name, exc,
|
||||
)
|
||||
result = {"exit_code": -1, "stdout": "", "stderr": str(exc)}
|
||||
|
||||
sync_log.status = "success" if result["exit_code"] == 0 else "failed"
|
||||
sync_log.finished_at = datetime.now(timezone.utc)
|
||||
|
||||
@@ -12,6 +12,13 @@ _STALE_RUNNING_MESSAGE = (
|
||||
)
|
||||
|
||||
|
||||
def _utc_aware(dt: datetime) -> datetime:
|
||||
"""MySQL DateTime columns are naive UTC; normalize for timedelta math."""
|
||||
if dt.tzinfo is None:
|
||||
return dt.replace(tzinfo=timezone.utc)
|
||||
return dt.astimezone(timezone.utc)
|
||||
|
||||
|
||||
class SyncLogRepositoryImpl:
|
||||
"""Async SQLAlchemy implementation of SyncLog data access"""
|
||||
|
||||
@@ -100,7 +107,7 @@ class SyncLogRepositoryImpl:
|
||||
log.error_message = _STALE_RUNNING_MESSAGE
|
||||
if log.duration_seconds == 0 and log.started_at:
|
||||
log.duration_seconds = max(
|
||||
0, int((now - log.started_at).total_seconds())
|
||||
0, int((now - _utc_aware(log.started_at)).total_seconds())
|
||||
)
|
||||
|
||||
await self.session.commit()
|
||||
|
||||
Reference in New Issue
Block a user