停止/删除改为纯本地操作,页面不再查询 Gitea
停止只在本平台标记已取消、删除只删本地记录,均不再调用 GitHub/Gitea 的查询或取消接口,操作秒响应;构建状态完全以工作流结束回调为准,未收到回调的在途任务由纯本地 6 小时超时兜底。 移除上一版的 Gitea 网页登录取消、网页账密配置项、runs 批量状态查询及两后端 get_run/cancel_run 接口,消除页面加载时的远程等待。
This commit is contained in:
+19
-98
@@ -2,7 +2,6 @@ import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
@@ -33,114 +32,35 @@ from .models import GithubRun, STATUS_LABELS, STATUS_BADGE
|
||||
|
||||
# 终态状态集合
|
||||
TERMINAL_STATUSES = ('success', 'failure', 'cancelled', 'timed_out', 'skipped')
|
||||
# 正常构建约 30~45 分钟;超过该时长平台仍显示进行中的,视为 runner 离线/作业僵死
|
||||
# 正常构建约 30~45 分钟;超过该时长仍在进行中的,视为 runner 离线/作业僵死。
|
||||
# 状态以构建脚本结束回调为准,页面不再向构建平台主动查询;超时兜底只做本地判断。
|
||||
STALE_BUILD_HOURS = 6
|
||||
# 列表页状态同步节流:同一在途任务两次主动查询的最小间隔,避免每次刷页面都串行请求平台
|
||||
REFRESH_THROTTLE_SECONDS = 20
|
||||
# 列表/仪表盘同步用短超时,平台抖动时不能拖垮页面
|
||||
REFRESH_TIMEOUT = 8
|
||||
_refresh_stamps = {}
|
||||
|
||||
|
||||
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 mark_stale_builds(gh_runs=None):
|
||||
"""纯本地兜底:在途任务超过 STALE_BUILD_HOURS 未收到结束回调的,标记为超时。
|
||||
|
||||
|
||||
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, timeout=timeout)
|
||||
except Exception as e:
|
||||
print(f"查询构建状态出错:{e}")
|
||||
return False
|
||||
if info is None:
|
||||
return False
|
||||
return _apply_run_info(gh_run, info)
|
||||
cutoff = timezone.now() - timedelta(hours=STALE_BUILD_HOURS)
|
||||
qs = GithubRun.objects.exclude(status__in=TERMINAL_STATUSES)
|
||||
if gh_runs is not None:
|
||||
ids = [r.pk for r in gh_runs if not r.is_finished()]
|
||||
qs = qs.filter(pk__in=ids)
|
||||
qs.filter(created_at__lt=cutoff).update(
|
||||
status='timed_out', updated_at=timezone.now())
|
||||
|
||||
|
||||
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'
|
||||
if new_status and new_status != gh_run.status:
|
||||
gh_run.status = new_status
|
||||
def mark_run_stale_if_needed(gh_run):
|
||||
"""单个任务的纯本地超时兜底(详情/状态接口用)。"""
|
||||
if not gh_run.is_finished() and gh_run.created_at < \
|
||||
timezone.now() - timedelta(hours=STALE_BUILD_HOURS):
|
||||
gh_run.status = 'timed_out'
|
||||
gh_run.save(update_fields=['status', 'updated_at'])
|
||||
return True
|
||||
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')
|
||||
@@ -502,7 +422,8 @@ 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, force=True, timeout=12)
|
||||
# 状态以构建脚本结束回调为准;这里只做纯本地的 6 小时超时兜底,不请求构建平台
|
||||
mark_run_stale_if_needed(gh_run)
|
||||
|
||||
return {
|
||||
"found": True,
|
||||
|
||||
Reference in New Issue
Block a user