7.3 KiB
7.3 KiB
推送失败重试 + 实时进度 — 设计规格
⚠️ 已归档 — 2026-05-22 本文档引用的文件结构已过时:
server/database.py已拆分为server/domain/models/;PHP 前端 (retry.php/servers.php) 已迁移到 Tailwind CSS v4 + Alpine.js;datetime.utcnow()已弃用。功能已实现,保留本文档供历史参考。
背景
当前推送 asyncio.gather 等待全部完成后一次性返回,大数据量时前端无反馈。失败推送只记录 SyncLog,需人工回到服务器管理页重试。需:实时进度 + 自动重试 + 一致性验证后消失。
核心设计
数据模型
新增 PushRetryJob 表:
class PushRetryJob(Base):
__tablename__ = "push_retry_jobs"
id = Column(Integer, primary_key=True, autoincrement=True)
server_id = Column(Integer, ForeignKey("servers.id"), nullable=False)
server_name = Column(String(100)) # 冗余,方便列表显示
source_path = Column(String(500))
target_path = Column(String(500))
status = Column(String(20), default="pending") # pending/retrying/abandoned
retry_count = Column(Integer, default=0)
max_retries = Column(Integer, default=100)
next_retry_at = Column(DateTime) # 初始 NOW()+5min
last_error = Column(Text)
created_at = Column(DateTime, default=datetime.datetime.utcnow)
updated_at = Column(DateTime, default=datetime.datetime.utcnow)
生命周期
推送失败 ──→ INSERT job (status=pending, next_retry=NOW+5min)
│
▼
后台 loop (60s) ──→ 扫描 next_retry_at <= NOW()
│
rsync -avz --stats (重新推送)
│
rsync -avzn --delete (一致性 dry-run)
│
┌───────────┴───────────┐
一致 不一致/错误
│ │
DELETE job retries < 100 ?
(消失) │
┌───────┴───────┐
是 否
│ │
next_retry status=abandoned
=NOW+5min (人工处理)
一致性验证命令
rsync -avzn --stats --delete --out-format='%i %n' source/ server:target/
-ndry-run--delete检测目标多余文件- 输出
Number of files: 0→ 完全一致
实时进度
sync_engine 改动:
push_to_server 新增 progress_callback 参数:
async def push_to_server(server, source, target, progress_callback=None):
# rsync --out-format='%i %n' --info=progress2
# 逐行解析:
# >f+++++++++ file.txt → 文件新增
# .f...p..... file2.txt → 权限变更
for line in iter(proc.stdout.readline, b''):
if m := re.match(r'^\S+\s+(.+)$', line):
file_name = m.group(1)
transferred += 1
if progress_callback:
await progress_callback(server.id, {
"file": file_name, "transferred": transferred,
"total": total_files, # 从 --stats 预取
})
WebSocket 推送路径:
push_to_server → progress_callback → WebSocket manager → 前端
→ broadcast({server_id, progress})
API 端点
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/servers/retry-jobs |
重试队列列表 |
| POST | /api/servers/retry-jobs/{id}/retry-now |
立即重试单条 |
| POST | /api/servers/retry-jobs/retry-all |
批量重试全部 pending |
| DELETE | /api/servers/retry-jobs/{id} |
删除/放弃单条 |
| DELETE | /api/servers/retry-jobs/clear-done |
清除已完成/放弃的 |
前端
retry.php — 重试队列页:
┌─ 重试队列 (3) ───────────────────────────────────────┐
│ 服务器 源目录 目标目录 重试 操作 │
│ web-01 /www/wwwroot/proj /var/www/site 2 🔄⏭ │
│ api-01 /www/wwwroot/api /opt/api 0 🔄⏭ │
│ db-01 /data/backup /backup 5 🔄⏭ │
│ [全部重试] [清除] │
└──────────────────────────────────────────────────────┘
servers.php 推送对话框改实时:
┌─ 推送中... ─────────────────────────────────┐
│ web-01 ████████████░░░░░░ 120/350 文件 │
│ 45MB · ETA 2 分钟 │
│ web-02 ████████████████░░ 200/350 文件 │
│ 80MB · ETA 1 分钟 │
│ 完成 1/3 │
│ [后台运行] │
└──────────────────────────────────────────────┘
sidebar 角标:重试队列 3
后台服务
新增 retry_engine.py:
async def retry_loop():
while True:
db = SessionLocal()
jobs = db.query(PushRetryJob).filter(
PushRetryJob.status == "pending",
PushRetryJob.next_retry_at <= datetime.utcnow(),
).all()
for job in jobs:
job.status = "retrying"
db.commit()
# rsync 推送
result = await push_to_server(server, job.source_path, job.target_path)
# dry-run 一致性
consistent = await verify_consistency(server, job.source_path, job.target_path)
if consistent:
db.delete(job)
else:
job.retry_count += 1
if job.retry_count >= job.max_retries:
job.status = "abandoned"
else:
job.status = "pending"
job.next_retry_at = datetime.utcnow() + timedelta(minutes=5)
job.last_error = result.get("message", "")
db.commit()
db.close()
await asyncio.sleep(60)
在 main.py lifespan 中启动 retry_loop。
文件统计
| 文件 | 改动 | 预估行数 |
|---|---|---|
server/database.py |
PushRetryJob 模型 | +15 |
server/services/retry_engine.py |
新建,retry loop + 一致性验证 | +120 |
server/api/servers.py |
push 端点 WebSocket 进度 + 失败写 job | +30 |
server/services/sync_engine.py |
流式解析 rsync + progress_callback | +40 |
server/main.py |
启动 retry_loop + WS manager | +15 |
web/retry.php |
新建 | +80 |
web/servers.php |
推送对话框实时进度条 | +60 |
| 各页面 sidebar | 加重试队列链接 + 角标 | +20 |
预估:8 文件,~380 行。
规格自检
- 无占位符/TODO
- 重试机制完整:失败 → INSERT → 定时 scan → rsync → verify → delete/retry/abandon
- 进度即时:WebSocket + callback 逐文件推送
- 一致性:rsync dry-run --delete,0 个变化才消失
- 前端独立页面 + sidebar 角标
- 不在推送对话框卡住用户