修复列表页加载慢并支持 Gitea 1.27 一键停止

1. 状态同步加 20 秒节流、超时缩短到 8 秒;Gitea 改用 runs 列表接口一次批量取回全部任务状态,在途任务并发补查作业,消除逐任务串行 API 请求导致的后台/我的构建/详情页慢。

2. 停止时先区分 404:运行已删除则本地标记取消;Gitea 1.27 无取消 API 时,若后台配置了 Gitea 网页账号密码则模拟网页登录取消,否则回退手动链接提示。

3. 后台 Gitea 连接设置新增网页登录账号/密码两个可选项。
This commit is contained in:
naeeo
2026-09-30 01:04:16 +08:00
parent 5ff6bcfe51
commit c8f41a34c0
6 changed files with 349 additions and 35 deletions
+81 -3
View File
@@ -2,6 +2,7 @@ import io
import json
import os
import re
import time
import uuid
from datetime import timedelta
from pathlib import Path
@@ -34,25 +35,52 @@ from .models import GithubRun, STATUS_LABELS, STATUS_BADGE
TERMINAL_STATUSES = ('success', 'failure', 'cancelled', 'timed_out', 'skipped')
# 正常构建约 30~45 分钟;超过该时长平台仍显示进行中的,视为 runner 离线/作业僵死
STALE_BUILD_HOURS = 6
# 列表页状态同步节流:同一在途任务两次主动查询的最小间隔,避免每次刷页面都串行请求平台
REFRESH_THROTTLE_SECONDS = 20
# 列表/仪表盘同步用短超时,平台抖动时不能拖垮页面
REFRESH_TIMEOUT = 8
_refresh_stamps = {}
def refresh_active_run(gh_run):
def _refresh_due(run_pk):
now = time.monotonic()
ts = _refresh_stamps.get(run_pk, 0)
if now - ts >= REFRESH_THROTTLE_SECONDS:
# 发起前先占位,避免并发请求堆积
_refresh_stamps[run_pk] = now
return True
return False
def refresh_active_run(gh_run, *, force=False, timeout=REFRESH_TIMEOUT):
"""向构建平台同步一次任务状态,状态落库后返回 True;网络异常或仍在进行则 False。
同时兜底两种平台侧的僵死:
- Gitea 前置作业失败后可复用工作流父作业/无匹配标签的作业长期挂起;
- 自建 runner 离线导致运行一直 in_progress(超过 STALE_BUILD_HOURS 判超时)。
force=False 时按 REFRESH_THROTTLE_SECONDS 节流(列表/仪表盘用);
force=True 跳过节流立即查询(详情页、停止操作后用),但同样刷新节流戳,
使紧接着的列表打开不再重复请求。
"""
if gh_run.is_finished() or not gh_run.github_run_id:
return False
if not force and not _refresh_due(gh_run.pk):
return False
_refresh_stamps[gh_run.pk] = time.monotonic()
backend = build_backends.get_backend(gh_run.backend or 'github')
try:
info = backend.get_run(gh_run.github_run_id)
info = backend.get_run(gh_run.github_run_id, timeout=timeout)
except Exception as e:
print(f"查询构建状态出错:{e}")
return False
if info is None:
return False
return _apply_run_info(gh_run, info)
def _apply_run_info(gh_run, info):
"""把平台返回的状态落库;终态与 6 小时僵死兜底均在此。返回是否有变化。"""
new_status = info.status if info.finished else None
if new_status is None and timezone.now() - gh_run.created_at > timedelta(hours=STALE_BUILD_HOURS):
new_status = 'timed_out'
@@ -63,6 +91,56 @@ def refresh_active_run(gh_run):
return False
def refresh_active_runs(gh_runs, *, force=False):
"""并发同步一批在途任务(列表页/仪表盘),整页只承受一次平台往返的耗时。
Gitea 优先用 runs 列表接口一次取回全部状态(在途任务再补 jobs 探测);
其他后端或批量接口不可用时退化为并发逐个查询。
"""
due = []
for run in gh_runs:
if run.is_finished() or not run.github_run_id:
continue
if _refresh_due(run.pk) or force:
_refresh_stamps[run.pk] = time.monotonic()
due.append(run)
if not due:
return
# 按后端分组批量
groups = {}
for run in due:
groups.setdefault(run.backend or 'github', []).append(run)
for backend_name, runs in groups.items():
backend = build_backends.get_backend(backend_name)
get_runs_batch = getattr(backend, 'get_runs_batch', None)
infos = None
if get_runs_batch is not None:
try:
infos = get_runs_batch(
[r.github_run_id for r in runs], timeout=REFRESH_TIMEOUT)
except Exception as e:
print(f"批量查询构建状态出错:{e}")
infos = None
if infos is None:
from concurrent.futures import ThreadPoolExecutor
workers = min(8, len(runs))
with ThreadPoolExecutor(max_workers=workers) as pool:
list(pool.map(lambda r: refresh_active_run(r, force=force), runs))
continue
missing = [run for run in runs if infos.get(str(run.github_run_id)) is None]
if missing:
# 列表窗口里没有的运行(已删除或超出最近 50 条):并发逐个查询兜底
from concurrent.futures import ThreadPoolExecutor
workers = min(8, len(missing))
with ThreadPoolExecutor(max_workers=workers) as pool:
list(pool.map(lambda r: refresh_active_run(r, force=True), missing))
for run in runs:
info = infos.get(str(run.github_run_id))
if info is not None:
_apply_run_info(run, info)
def _webhook_denied(request):
"""配置了 WEBHOOK_SECRET 时,校验 GitHub Actions 回调请求头;未配置则放行(兼容旧工作流)。"""
secret = app_settings.get_value('WEBHOOK_SECRET')
@@ -424,7 +502,7 @@ def _get_run_status(uuid_val):
backend = build_backends.get_backend(gh_run.backend or 'github')
github_log_url = backend.log_url(gh_run.github_run_id) if gh_run.github_run_id else ''
refresh_active_run(gh_run)
refresh_active_run(gh_run, force=True, timeout=12)
return {
"found": True,