Files
Agent-System-Dev/tests/load/collaboration/seed/main.go
T
weichenandCursor b6d57b9f8e Add B5 load seed tool and C2 dual-instance load entry
Why: U7 release checklist B5/C2 needed volume seed and dual-serve load wiring before capacity windows.
Evidence: tiny single/dual smoke (skip-k6) SSE p95≈4ms, dup_seq=0; seed COPY 1k msgs/chunks in ~30ms.
Impact: run_load.sh --seed/--instances; docs/release-multi-instance B5/C2 → ready; no backend product change.
Refs: docs/proposals/2026-10-11-collaboration-storage-and-distribution.md §3/§5 U7; docs/release-multi-instance.md
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 05:27:19 +00:00

347 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 协作存储 U7/B5:大数据量 seed 工具(方案 §3 数据量档)。
//
// 直连隔离 PG(不经 HTTP),用 COPY 灌 messages/events/knowledge。
// 用户须已由 run_load.sh 经 CLI 创建(口令哈希与现网一致);本工具只写
// 项目/会话拓扑与体量数据。不写 knowledge_vectors(向量扫描面向数万块)。
//
// 用法:
// seed --dsn 'postgresql://…/u7load_app' --profile tiny|contract [--manifest path]
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type profile struct {
users int
projects int // per user
convs int // per project
messages int
chunks int
sharedPct int // % of knowledge with owner=''
}
var profiles = map[string]profile{
// 接线冒烟:秒级。
"tiny": {users: 3, projects: 2, convs: 2, messages: 1000, chunks: 1000, sharedPct: 10},
// 负载合同数据量档(约需数分钟与数 GB 磁盘,视机器而定)。
"contract": {users: 10, projects: 5, convs: 10, messages: 1_000_000, chunks: 100_000, sharedPct: 10},
}
type manifest struct {
Profile string `json:"profile"`
Owners []string `json:"owners"`
Projects int `json:"projects"`
Conversations int `json:"conversations"`
Messages int `json:"messages"`
Events int `json:"events"`
Knowledge int `json:"knowledge"`
SharedChunks int `json:"sharedChunks"`
ElapsedMS int64 `json:"elapsedMs"`
StartedAt time.Time `json:"startedAt"`
FinishedAt time.Time `json:"finishedAt"`
}
func main() {
dsn := flag.String("dsn", "", "PostgreSQL DSN(必填;目标须已迁移)")
profName := flag.String("profile", "tiny", "tiny|contract")
manifestPath := flag.String("manifest", "", "可选:写出 seed-manifest.json")
usersOverride := flag.Int("users", 0, "覆盖用户数(0=用 profile)")
projectsOverride := flag.Int("projects", 0, "覆盖每用户项目数")
messagesOverride := flag.Int("messages", 0, "覆盖消息总数")
chunksOverride := flag.Int("chunks", 0, "覆盖知识分块总数")
flag.Parse()
if *dsn == "" {
fmt.Fprintln(os.Stderr, "seed: --dsn required")
os.Exit(2)
}
p, ok := profiles[*profName]
if !ok {
fmt.Fprintf(os.Stderr, "seed: unknown profile %q (tiny|contract)\n", *profName)
os.Exit(2)
}
if *usersOverride > 0 {
p.users = *usersOverride
}
if *projectsOverride > 0 {
p.projects = *projectsOverride
}
if *messagesOverride > 0 {
p.messages = *messagesOverride
}
if *chunksOverride > 0 {
p.chunks = *chunksOverride
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, *dsn)
if err != nil {
fmt.Fprintln(os.Stderr, "seed: connect:", err)
os.Exit(1)
}
defer pool.Close()
if err := requireSchema(ctx, pool); err != nil {
fmt.Fprintln(os.Stderr, "seed:", err)
os.Exit(1)
}
owners := make([]string, p.users)
for i := 0; i < p.users; i++ {
owners[i] = fmt.Sprintf("seed_u%d", i)
}
if err := requireOwners(ctx, pool, owners); err != nil {
fmt.Fprintln(os.Stderr, "seed:", err)
os.Exit(1)
}
started := time.Now().UTC()
fmt.Printf("[seed] profile=%s users=%d projects/user=%d convs/project=%d messages=%d chunks=%d\n",
*profName, p.users, p.projects, p.convs, p.messages, p.chunks)
projIDs, convIDs, err := insertTopology(ctx, pool, owners, p.projects, p.convs)
if err != nil {
fmt.Fprintln(os.Stderr, "seed: topology:", err)
os.Exit(1)
}
fmt.Printf("[seed] topology: projects=%d conversations=%d\n", len(projIDs), len(convIDs))
nMsg, err := copyMessagesEvents(ctx, pool, convIDs, p.messages)
if err != nil {
fmt.Fprintln(os.Stderr, "seed: messages/events:", err)
os.Exit(1)
}
fmt.Printf("[seed] messages=%d events=%d\n", nMsg, nMsg)
nKnw, nShared, err := copyKnowledge(ctx, pool, projIDs, p.chunks, p.sharedPct)
if err != nil {
fmt.Fprintln(os.Stderr, "seed: knowledge:", err)
os.Exit(1)
}
fmt.Printf("[seed] knowledge=%d (shared owner=''=%d)\n", nKnw, nShared)
finished := time.Now().UTC()
m := manifest{
Profile: *profName,
Owners: owners,
Projects: len(projIDs),
Conversations: len(convIDs),
Messages: nMsg,
Events: nMsg,
Knowledge: nKnw,
SharedChunks: nShared,
ElapsedMS: finished.Sub(started).Milliseconds(),
StartedAt: started,
FinishedAt: finished,
}
if *manifestPath != "" {
b, _ := json.MarshalIndent(m, "", " ")
if err := os.WriteFile(*manifestPath, b, 0o644); err != nil {
fmt.Fprintln(os.Stderr, "seed: write manifest:", err)
os.Exit(1)
}
fmt.Printf("[seed] manifest=%s\n", *manifestPath)
}
fmt.Printf("[seed] done in %dms\n", m.ElapsedMS)
}
func requireSchema(ctx context.Context, pool *pgxpool.Pool) error {
for _, t := range []string{"projects", "conversations", "messages", "events", "knowledge", "users"} {
var one int
if err := pool.QueryRow(ctx, `SELECT 1 FROM information_schema.tables WHERE table_schema='public' AND table_name=$1`, t).Scan(&one); err != nil {
return fmt.Errorf("schema missing table %s (run migrations via agent-system user create first): %w", t, err)
}
}
return nil
}
func requireOwners(ctx context.Context, pool *pgxpool.Pool, owners []string) error {
for _, o := range owners {
var one int
err := pool.QueryRow(ctx, `SELECT 1 FROM users WHERE owner=$1`, o).Scan(&one)
if err != nil {
return fmt.Errorf("user %q not found — run_load.sh must CLI-create seed_u* before seed", o)
}
}
return nil
}
type projectRow struct {
id, owner string
}
func insertTopology(ctx context.Context, pool *pgxpool.Pool, owners []string, projectsPerUser, convsPerProject int) (projects []projectRow, convIDs []string, err error) {
now := time.Now().UTC().Format(time.RFC3339Nano)
tx, err := pool.Begin(ctx)
if err != nil {
return nil, nil, err
}
defer tx.Rollback(ctx)
var projBuf [][]any
var convBuf [][]any
for ui, owner := range owners {
for pi := 0; pi < projectsPerUser; pi++ {
pid := fmt.Sprintf("prj_seed_%d_%d", ui, pi)
projBuf = append(projBuf, []any{pid, owner, fmt.Sprintf("seed-proj-%d-%d", ui, pi), "{}", now, now})
projects = append(projects, projectRow{id: pid, owner: owner})
for ci := 0; ci < convsPerProject; ci++ {
cid := fmt.Sprintf("cnv_seed_%d_%d_%d", ui, pi, ci)
convBuf = append(convBuf, []any{cid, pid, owner, fmt.Sprintf("seed-conv-%d-%d-%d", ui, pi, ci), "", now, now})
convIDs = append(convIDs, cid)
}
}
}
if _, err := tx.CopyFrom(ctx,
pgx.Identifier{"projects"},
[]string{"id", "owner", "name", "settings", "created_at", "updated_at"},
pgx.CopyFromRows(projBuf)); err != nil {
return nil, nil, fmt.Errorf("projects: %w", err)
}
if _, err := tx.CopyFrom(ctx,
pgx.Identifier{"conversations"},
[]string{"id", "project_id", "owner", "title", "profile_id", "created_at", "updated_at"},
pgx.CopyFromRows(convBuf)); err != nil {
return nil, nil, fmt.Errorf("conversations: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return nil, nil, err
}
return projects, convIDs, nil
}
// copyMessagesEvents 均匀分配到各会话,每会话连续 seq 从 1 起;每条消息配对一条 event。
func copyMessagesEvents(ctx context.Context, pool *pgxpool.Pool, convIDs []string, total int) (int, error) {
if total <= 0 || len(convIDs) == 0 {
return 0, nil
}
const batch = 10000
now := time.Now().UTC().Format(time.RFC3339Nano)
perConv := make([]int, len(convIDs))
for i := 0; i < total; i++ {
perConv[i%len(convIDs)]++
}
written := 0
msgBuf := make([][]any, 0, batch)
evBuf := make([][]any, 0, batch)
flush := func() error {
if len(msgBuf) == 0 {
return nil
}
tx, err := pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err := tx.CopyFrom(ctx,
pgx.Identifier{"messages"},
[]string{"id", "conversation_id", "run_id", "role", "parts", "seq", "created_at", "request_id"},
pgx.CopyFromRows(msgBuf)); err != nil {
return fmt.Errorf("messages copy: %w", err)
}
if _, err := tx.CopyFrom(ctx,
pgx.Identifier{"events"},
[]string{"conversation_id", "seq", "run_id", "ts", "v", "type", "payload"},
pgx.CopyFromRows(evBuf)); err != nil {
return fmt.Errorf("events copy: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return err
}
msgBuf = msgBuf[:0]
evBuf = evBuf[:0]
return nil
}
global := 0
for ci, cid := range convIDs {
for seq := 1; seq <= perConv[ci]; seq++ {
global++
role := "user"
if seq%2 == 0 {
role = "assistant"
}
parts := fmt.Sprintf(`[{"type":"text","text":"seed msg %d"}]`, global)
mid := fmt.Sprintf("msg_seed_%d", global)
msgBuf = append(msgBuf, []any{mid, cid, "", role, parts, seq, now, ""})
payload := fmt.Sprintf(`{"messageId":%q,"seq":%d}`, mid, seq)
evBuf = append(evBuf, []any{cid, seq, "", now, 1, "message.upserted", payload})
written++
if len(msgBuf) >= batch {
if err := flush(); err != nil {
return written, err
}
if written%100000 == 0 {
fmt.Printf("[seed] … messages %d/%d\n", written, total)
}
}
}
}
if err := flush(); err != nil {
return written, err
}
return written, nil
}
func copyKnowledge(ctx context.Context, pool *pgxpool.Pool, projects []projectRow, total, sharedPct int) (n, shared int, err error) {
if total <= 0 || len(projects) == 0 {
return 0, 0, nil
}
const batch = 5000
now := time.Now().UTC().Format(time.RFC3339Nano)
buf := make([][]any, 0, batch)
flush := func() error {
if len(buf) == 0 {
return nil
}
tx, err := pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err := tx.CopyFrom(ctx,
pgx.Identifier{"knowledge"},
[]string{"id", "owner", "project_id", "source", "title", "ref", "chunk", "created_at"},
pgx.CopyFromRows(buf)); err != nil {
return err
}
if err := tx.Commit(ctx); err != nil {
return err
}
buf = buf[:0]
return nil
}
for i := 0; i < total; i++ {
pr := projects[i%len(projects)]
owner := pr.owner
if sharedPct > 0 && i%100 < sharedPct {
owner = ""
shared++
}
id := fmt.Sprintf("knw_seed_%d", i+1)
chunk := fmt.Sprintf("seed knowledge chunk %d about project %s topic %d", i+1, pr.id, i%97)
buf = append(buf, []any{id, owner, pr.id, "seed", fmt.Sprintf("seed-doc-%d", i+1), id, chunk, now})
n++
if len(buf) >= batch {
if err := flush(); err != nil {
return n, shared, err
}
}
}
if err := flush(); err != nil {
return n, shared, err
}
return n, shared, nil
}