From 5ed3c284912034995bcb7cfda085c0ae3f1f055b Mon Sep 17 00:00:00 2001 From: Nexus Agent Date: Mon, 8 Jun 2026 02:14:33 +0800 Subject: [PATCH] =?UTF-8?q?fix(sync):=20=E6=8E=A8=E9=80=81=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E6=97=B6=E8=90=BD=E5=BA=93=20failed=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E8=AE=A2=E6=AD=A3=E6=97=B6=E5=8C=BA=E4=B8=8E=E5=8D=A1?= =?UTF-8?q?=E4=BD=8F=E9=98=88=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rsync 抛错时 sync_log 不再永久 running;reconcile 兼容 naive UTC。 Co-authored-by: Cursor --- .../2026-06-07-sync-log-stale-running-fix.md | 32 +++++++++++++++++++ frontend/src/composables/push/usePushLogs.ts | 2 +- server/application/services/sync_engine_v2.py | 9 +++++- .../infrastructure/database/sync_log_repo.py | 9 +++++- 4 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 docs/changelog/2026-06-07-sync-log-stale-running-fix.md diff --git a/docs/changelog/2026-06-07-sync-log-stale-running-fix.md b/docs/changelog/2026-06-07-sync-log-stale-running-fix.md new file mode 100644 index 00000000..47369cd3 --- /dev/null +++ b/docs/changelog/2026-06-07-sync-log-stale-running-fix.md @@ -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` diff --git a/frontend/src/composables/push/usePushLogs.ts b/frontend/src/composables/push/usePushLogs.ts index f50d35a7..5b810000 100644 --- a/frontend/src/composables/push/usePushLogs.ts +++ b/frontend/src/composables/push/usePushLogs.ts @@ -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} 条卡住的推送记录` : '没有需要订正的记录') diff --git a/server/application/services/sync_engine_v2.py b/server/application/services/sync_engine_v2.py index 04e75874..2e395fae 100644 --- a/server/application/services/sync_engine_v2.py +++ b/server/application/services/sync_engine_v2.py @@ -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) diff --git a/server/infrastructure/database/sync_log_repo.py b/server/infrastructure/database/sync_log_repo.py index 761302d5..24c77b74 100644 --- a/server/infrastructure/database/sync_log_repo.py +++ b/server/infrastructure/database/sync_log_repo.py @@ -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()