Why: collaboration storage U7 (proposal sections 3/5/7) requires two-process HTTP-level acceptance over shared PG, a reproducible load-test entry honoring the load contract, and an auditable pre-release checklist; capacity windows and dual-host runs need user-scheduled resources, so the deliverable here is the verifiable entry, not capacity claims. Evidence: make test green (composite); make test-multi-instance green locally against an isolated PG (five scenarios incl. crash-takeover and restart invariants); run_load.sh --profile smoke end-to-end green (SSE probe p95~18ms, zero seq duplicates, stack auto-recycled); backend pinned to feature branch with the three cross-instance fixes. Impact: new tests/integration/multi-instance (CI test-pg job step), new tests/load/collaboration (k6 + SSE probe + run_load.sh), release checklist at docs/release-multi-instance.md; no runtime behavior change in Dev repo itself. Refs: backend PR #164; task card feature/u7-load-fault-release.
141 lines
6.4 KiB
Bash
Executable File
141 lines
6.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 协作存储 U7 压测入口(方案 §3 负载合同 / §5 U7)。
|
||
#
|
||
# 起一个隔离栈(backend mock 单实例 + 隔离 PG)→ 预建引导账号 → k6 阶梯
|
||
# → SSE 可见延迟探针 → 数据不变量对账 → 结果落 results/<ts>/ → 回收。
|
||
# 绝不打生产服务:默认自建 PG 实例(svc.sh);--dsn 复用已有隔离实例。
|
||
#
|
||
# 用法:
|
||
# tests/load/collaboration/run_load.sh [--profile smoke|ladder|sustained60m|sustained2h]
|
||
# [--users N] [--dsn <PG DSN>] [--port <app 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
|
||
[[ $KEEP -eq 1 ]] && echo "[load] --keep:栈保留(PG=u7load:$PG_PORT,app=127.0.0.1:$APP_PORT,结果 $RESULTS)"
|
||
}
|
||
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"
|