#!/usr/bin/env bash # 协作存储 U7 压测入口(方案 §3 负载合同 / §5 U7)。 # # 起一个隔离栈(backend mock 单实例 + 隔离 PG)→ 预建引导账号 → k6 阶梯 # → SSE 可见延迟探针 → 数据不变量对账 → 结果落 results// → 回收。 # 绝不打生产服务:默认自建 PG 实例(svc.sh);--dsn 复用已有隔离实例。 # # 用法: # tests/load/collaboration/run_load.sh [--profile smoke|ladder|sustained60m|sustained2h] # [--users N] [--dsn ] [--port ] # [--keep] [--skip-k6] [--skip-probe] [--help] # 依赖:bash4、go(构建 backend/探针)、psql(对账)、curl; # k6 可选(缺失时跳过 k6 段并给出安装指引,探针与对账照跑)。 set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" HERE="$ROOT/tests/load/collaboration" PROFILE=smoke USERS=30 APP_PORT=18980 PG_PORT=18981 KEEP=0 SKIP_K6=0 SKIP_PROBE=0 DSN="" while [[ $# -gt 0 ]]; do case "$1" in --profile) PROFILE="$2"; shift 2 ;; --users) USERS="$2"; shift 2 ;; --dsn) DSN="$2"; shift 2 ;; --port) APP_PORT="$2"; shift 2 ;; --keep) KEEP=1; shift ;; --skip-k6) SKIP_K6=1; shift ;; --skip-probe) SKIP_PROBE=1; shift ;; -h|--help) sed -n '2,16p' "$0"; exit 0 ;; *) echo "unknown arg: $1" >&2; exit 2 ;; esac done TS="$(date +%Y%m%d-%H%M%S)" RESULTS="$HERE/results/$TS" mkdir -p "$RESULTS" OWNED_PG=0 APP_PID="" cleanup() { [[ -n "$APP_PID" ]] && kill "$APP_PID" 2>/dev/null || true if [[ $OWNED_PG -eq 1 && $KEEP -eq 0 ]]; then "$ROOT/scripts/svc.sh" u7load stop >/dev/null 2>&1 || true "$ROOT/scripts/svc.sh" rm u7load --purge >/dev/null 2>&1 || true fi if [[ $KEEP -eq 1 ]]; then echo "[load] --keep:栈保留(PG=u7load:$PG_PORT,app=127.0.0.1:$APP_PORT,结果 $RESULTS)" fi return 0 2>/dev/null || true } trap cleanup EXIT # ---- 1) PG:自建隔离实例(或复用 --dsn) ---- if [[ -z "$DSN" ]]; then echo "[load] 创建隔离 PG 实例 u7load(端口 $PG_PORT)" "$ROOT/scripts/svc.sh" db create u7load --port "$PG_PORT" >/dev/null OWNED_PG=1 "$ROOT/scripts/svc.sh" u7load start >/dev/null DSN="$("$ROOT/scripts/svc.sh" db dsn u7load --reveal)" fi echo "[load] PG DSN 已就绪(密码不回显)" # ---- 2) 构建 backend 与探针 ---- echo "[load] 构建 backend(worktree 当前源码)与 SSE 探针" BIN="$HERE/.build/agent-system" mkdir -p "$HERE/.build" ( cd "$ROOT/modules/backend" && CGO_ENABLED=0 go build -o "$BIN" ./cmd/agent-system ) ( cd "$HERE/sse_probe" && go build -o "$HERE/.build/sse_probe" . ) # ---- 3) 压测库 + 引导账号(CLI 直连,终态口令避免首登改密分支) ---- # DSN 规范化:库名换 u7load_app、query 补 sslmode=disable(密码不回显)。 ADMIN_DSN="$(python3 - "$DSN" <<'PY' import sys from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode u = urlsplit(sys.argv[1]) q = dict(parse_qsl(u.query)) q.setdefault("sslmode", "disable") print(urlunsplit((u.scheme, u.netloc, "/" + (u.path.lstrip("/") or "postgres"), urlencode(q), ""))) PY )" psql "$ADMIN_DSN" -v ON_ERROR_STOP=1 -c "DROP DATABASE IF EXISTS u7load_app WITH (FORCE)" -c "CREATE DATABASE u7load_app" >/dev/null APP_DSN="$(python3 - "$ADMIN_DSN" <<'PY' import sys from urllib.parse import urlsplit, urlunsplit u = urlsplit(sys.argv[1]) print(urlunsplit((u.scheme, u.netloc, "/u7load_app", u.query, ""))) PY )" "$BIN" user create --dsn "$APP_DSN" --admin --owner load_admin --password load-admin-pass-Aa1 >/dev/null "$BIN" user create --dsn "$APP_DSN" --owner probe_user --password probe-user-pass-Aa9 >/dev/null # ---- 4) 起被测实例(mock、单实例首期;执行原语关闭——压测不打执行面) ---- echo "[load] 启动被测实例 127.0.0.1:$APP_PORT(mock / tier=team / auth=on)" APP_LOG="$RESULTS/app.log" "$BIN" serve --dsn "$APP_DSN" --addr "127.0.0.1:$APP_PORT" --tier team --auth on --mock \ --instance u7load --disable-exec-tools >"$APP_LOG" 2>&1 & APP_PID=$! # auth=on 时 /api/health 同样要求鉴权:200/401 都视为已就绪(服务在正确应答)。 app_ready() { code=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:$APP_PORT/api/health"); [[ "$code" == 200 || "$code" == 401 ]]; } for _ in $(seq 1 60); do app_ready && break; sleep 0.5; done app_ready || { echo "[load] app 未就绪,日志 $APP_LOG" >&2; exit 1; } BASE="http://127.0.0.1:$APP_PORT" # ---- 5) k6 阶梯 ---- if [[ $SKIP_K6 -eq 1 ]]; then echo "[load] --skip-k6:跳过 k6 段" elif ! command -v k6 >/dev/null 2>&1; then cat >"$RESULTS/k6-skipped.txt" <<'EOF' k6 未安装:本段跳过(探针与对账照跑)。 安装(本机 linuxbrew):brew install k6 安装后重跑:tests/load/collaboration/run_load.sh --profile <同参数> EOF echo "[load] k6 未安装,跳过(见 $RESULTS/k6-skipped.txt)" else echo "[load] k6 profile=$PROFILE users=$USERS" ( cd "$HERE" && BASE_URL="$BASE" ADMIN_OWNER=load_admin ADMIN_PASS=load-admin-pass-Aa1 \ USERS="$USERS" PROFILE="$PROFILE" \ k6 run --summary-export "$RESULTS/k6-summary.json" k6/collab_api.js ) | tee "$RESULTS/k6-output.txt" fi # ---- 6) SSE 可见延迟探针 ---- if [[ $SKIP_PROBE -eq 0 ]]; then echo "[load] SSE 可见延迟探针(60s,10 会话)" "$HERE/.build/sse_probe" -base "$BASE" -dur 60s -convs 10 -out "$RESULTS/sse-probe.json" fi # ---- 7) 数据不变量对账(方案 §3:零违反) ---- echo "[load] 数据不变量对账" psql "$APP_DSN" -v ON_ERROR_STOP=1 \ -c "SELECT 'users' k, COUNT(*) v FROM users UNION ALL SELECT 'projects', COUNT(*) FROM projects UNION ALL SELECT 'conversations', COUNT(*) FROM conversations UNION ALL SELECT 'messages', COUNT(*) FROM messages UNION ALL SELECT 'events', COUNT(*) FROM events UNION ALL SELECT 'runs', COUNT(*) FROM runs" \ -c "SELECT 'dup_seq_per_conv' k, COUNT(*) v FROM (SELECT conversation_id, seq FROM events GROUP BY 1,2 HAVING COUNT(*)>1) d" \ -c "SELECT 'nonterminal_orphan_runs' k, COUNT(*) v FROM runs WHERE status IN ('pending','running','waiting_hitl')" \ | tee "$RESULTS/invariants.txt" DUP=$(psql "$APP_DSN" -tAc "SELECT COUNT(*) FROM (SELECT conversation_id, seq FROM events GROUP BY 1,2 HAVING COUNT(*)>1) d") if [[ "$DUP" != "0" ]]; then echo "[load] 对账失败:events 每会话 seq 出现重复($DUP 组)" >&2 exit 1 fi echo "[load] 对账通过:无重复 seq;孤儿 run 报告见 invariants.txt(探针正常收口应为 0)" echo "[load] 完成。结果目录:$RESULTS"