fix(sync): 推送异常时落库 failed,修复订正时区与卡住阈值

rsync 抛错时 sync_log 不再永久 running;reconcile 兼容 naive UTC。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Agent
2026-06-08 02:14:33 +08:00
parent 77cd9a5205
commit 5ed3c28491
4 changed files with 49 additions and 3 deletions
@@ -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`
+1 -1
View File
@@ -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()