Production-grade: webhook execution, Postgres backup, rate limit, worker timeout/retry
GitHub webhook: delivery-id dedup persisted in webhook_deliveries, /fix-review creates a queued task with audit (no model run in the webhook itself), check_run/check_suite parsing without mutating task state (CI backfill still via pr-checks). Control plane: Postgres pg_dump backup branch + POST /admin/backup (maintainer+, password only via PGPASSWORD env), per-key/IP rate limit (default 120/min, 429), health returns backend + db_ok, metrics by_status always has all seven status keys. Worker: per-task timeout (default 1800s, --task-timeout), timeout is failed not interrupted, limited retries (default 1, backoff, fallback failures not re-retried), stale codex- container cleanup on loop/CLI start. Tests: cloud 271 (267 pass, 4 skipped are pre-existing real- Postgres tests), host 27 pass.
This commit is contained in:
1 parent
86160ad175
commit
33841355b5
9 files changed
+1366
-52
No files matched your search
+63
-2
@@ -15,7 +15,9 @@
|
||||
import argparse
|
||||
import glob
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from cloud.db import connect as _db_connect
|
||||
@@ -50,13 +52,72 @@ def backup_db(db_path, out_dir, keep=3, timestamp=None):
|
||||
return dest
|
||||
|
||||
|
||||
def backup_postgres(out_dir, keep=3, timestamp=None, runner=None,
|
||||
pg_dump_cmd="pg_dump"):
|
||||
"""Postgres 备份:调 pg_dump 写出 .sql 文件并轮转,返回文件路径。
|
||||
|
||||
连接参数只走 CLOUD_DB_* 环境变量(host/port/dbname/user/
|
||||
password),密码只以 PGPASSWORD 环境变量传给子进程,绝不
|
||||
出现在命令行参数、返回值或日志里。runner 可注入(单测用假
|
||||
runner):调用约定 runner(cmd, env) -> 具 returncode/stdout/
|
||||
stderr 的对象;未注入时用 subprocess.run。pg_dump 不存在时
|
||||
明确报错;pg_dump 非零退出时明确报错(stderr 截断,不含密码)。
|
||||
轮转规则与 SQLite 分支一致:只留最近 keep 份,keep < 1 报错。
|
||||
SQLite 分支原样不动,本函数不碰任何 .db 文件。
|
||||
"""
|
||||
if keep < 1:
|
||||
raise ValueError("keep 必须 >= 1,否则会删光所有备份")
|
||||
host = os.environ.get("CLOUD_DB_HOST") or "127.0.0.1"
|
||||
port = os.environ.get("CLOUD_DB_PORT") or "5432"
|
||||
dbname = os.environ.get("CLOUD_DB_NAME")
|
||||
user = os.environ.get("CLOUD_DB_USER")
|
||||
password = os.environ.get("CLOUD_DB_PASSWORD")
|
||||
if not dbname or not user:
|
||||
raise RuntimeError(
|
||||
"Postgres 备份需要 CLOUD_DB_NAME 与 CLOUD_DB_USER")
|
||||
if shutil.which(pg_dump_cmd) is None and runner is None:
|
||||
raise RuntimeError("pg_dump 不可用:本机找不到 %s" % pg_dump_cmd)
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
stamp = timestamp or time.strftime("%Y%m%d%H%M%S", time.localtime())
|
||||
dest = os.path.join(out_dir, "cloud-backup-%s.sql" % stamp)
|
||||
cmd = [pg_dump_cmd, "-h", host, "-p", str(port), "-U", user,
|
||||
"-d", dbname, "-f", dest]
|
||||
env = dict(os.environ)
|
||||
if password:
|
||||
env["PGPASSWORD"] = password
|
||||
if runner is not None:
|
||||
result = runner(cmd, env)
|
||||
else:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True,
|
||||
env=env)
|
||||
if getattr(result, "returncode", 1) != 0:
|
||||
err = (getattr(result, "stderr", "") or "")[:500]
|
||||
raise RuntimeError("pg_dump 失败:%s" % err)
|
||||
if not os.path.exists(dest):
|
||||
raise RuntimeError("pg_dump 未生成备份文件:%s" % dest)
|
||||
backups = sorted(glob.glob(os.path.join(out_dir, "cloud-backup-*.sql")))
|
||||
for old in backups[:-keep] if len(backups) > keep else []:
|
||||
os.remove(old)
|
||||
return dest
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description="cloud control-plane backup")
|
||||
parser.add_argument("--db", required=True)
|
||||
parser.add_argument("--db", default=None,
|
||||
help="SQLite 库路径;postgres 模式可省略")
|
||||
parser.add_argument("--out", required=True)
|
||||
parser.add_argument("--keep", type=int, default=3)
|
||||
parser.add_argument("--backend", default="sqlite",
|
||||
choices=["sqlite", "postgres"],
|
||||
help="sqlite 时 --db 必填;postgres 时忽略 --db,"
|
||||
"连接参数走 CLOUD_DB_* 环境变量")
|
||||
args = parser.parse_args(argv)
|
||||
dest = backup_db(args.db, args.out, keep=args.keep)
|
||||
if args.backend == "postgres":
|
||||
dest = backup_postgres(args.out, keep=args.keep)
|
||||
else:
|
||||
if not args.db:
|
||||
parser.error("sqlite 模式需要 --db")
|
||||
dest = backup_db(args.db, args.out, keep=args.keep)
|
||||
print(dest)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user