Files
Agent-System-Backend/internal/store/store.go
T
weichen 29f14ac5f2 Add multi-instance groundwork: blobstore seam for artifacts
Details (original message):
Add multi-instance groundwork: blobstore seam for artifacts, PG LISTEN/NOTIFY event fanout, and per-tenant rate limiting with native Redis backend
2026-09-30 02:39:50 +00:00

149 lines
5.1 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.
// Package store 是持久化层唯一入口(存储档接口,设计 4.1/D5/D16)。
// 两档实现共用同一方法面:
// - 嵌入式档 SQLite(默认:单文件 + 本地对象目录,零外部服务);
// - PostgreSQL 档(多用户/多实例;DSN 配置,pgx 纯 Go 驱动,静态构建兼容)。
//
// 方言差异(占位符、UPSERT、全文检索、备份、咨询锁)收敛在 dialect 与方言文件;
// 业务层只依赖本包公开方法。schema 演进走版本化迁移(migrate.go,双方言、可回滚)。
// 租户硬边界:租户域查询一律带 owner 过滤,跨租户访问返回 ErrNotFound。
package store
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"agent-system-backend/internal/blobstore"
_ "github.com/jackc/pgx/v5/stdlib" // PG 档驱动(注册名 "pgx")
_ "modernc.org/sqlite" // 嵌入式档驱动(注册名 "sqlite",CGO-free)
)
var ErrNotFound = errors.New("store: not found")
// OwnerDefault 是单租户默认 owner(嵌入式档 / 未启用认证时的隐式身份)。
const OwnerDefault = "local"
type Store struct {
db *sql.DB
dialect dialect
dataDir string // 工件对象目录(备份路径等诊断用)
blobs blobstore.Store // 工件对象存储(本地档;多实例切共享卷/对象存储)
}
// OpenSQLite 打开(必要时创建)嵌入式存储并执行迁移。
func OpenSQLite(path string) (*Store, error) {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return nil, err
}
dsn := fmt.Sprintf("file:%s?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)", path)
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, err
}
// SQLite 单写者:串行化连接,语句级原子 + 版本化迁移事务。
db.SetMaxOpenConns(1)
return finishOpen(db, sqliteDialect{}, filepath.Join(filepath.Dir(path), "artifacts"), nil)
}
// OpenPostgres 打开 PostgreSQL 档存储并执行迁移。
// DSN 例:postgres://user:pass@host:5432/agent_system?sslmode=disable
// artifactsDir 为工件对象目录(多实例部署时指向共享卷;单实例默认本地)。
func OpenPostgres(dsn, artifactsDir string) (*Store, error) {
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(16)
db.SetMaxIdleConns(8)
db.SetConnMaxIdleTime(5 * time.Minute)
return finishOpen(db, postgresDialect{}, artifactsDir, nil)
}
// OpenPostgresWithBlobs 是注入自定义对象存储的 PG 档入口(多实例共享卷/对象存储)。
func OpenPostgresWithBlobs(dsn, artifactsDir string, blobs blobstore.Store) (*Store, error) {
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(16)
db.SetMaxIdleConns(8)
db.SetConnMaxIdleTime(5 * time.Minute)
return finishOpen(db, postgresDialect{}, artifactsDir, blobs)
}
func finishOpen(db *sql.DB, d dialect, artifactsDir string, blobs blobstore.Store) (*Store, error) {
if blobs == nil {
var err error
blobs, err = blobstore.NewLocal(artifactsDir)
if err != nil {
db.Close()
return nil, err
}
}
s := &Store{db: db, dialect: d, dataDir: artifactsDir, blobs: blobs}
if err := s.migrate(); err != nil {
db.Close()
return nil, err
}
if err := os.MkdirAll(artifactsDir, 0o755); err != nil {
db.Close()
return nil, err
}
return s, nil
}
func (s *Store) Close() error { return s.db.Close() }
// Dialect 返回当前存储档名(sqlite | postgres;诊断/健康检查用)。
func (s *Store) Dialect() string { return s.dialect.name() }
// DataDir 返回工件对象目录。
func (s *Store) DataDir() string { return s.dataDir }
// BlobStore 返回工件对象存储(诊断/运维)。
func (s *Store) BlobStore() blobstore.Store { return s.blobs }
// NotifyPG 发送 PostgreSQL NOTIFY(PG 档事件扇出用;SQLite 档返回未执行)。
func (s *Store) NotifyPG(ctx context.Context, channel, payload string) (bool, error) {
if s.dialect.name() != "postgres" {
return false, nil
}
_, err := s.db.ExecContext(ctx, `SELECT pg_notify($1, $2)`, channel, payload)
return true, err
}
// q 按方言重绑占位符(查询统一用 ? 书写,PG 执行前转 $n)。
// 本包 SQL 不含字符串字面量 "?",重绑安全。
func (s *Store) q(query string) string { return s.dialect.rebind(query) }
// Backup 生成一致性备份文件。SQLite 档用 VACUUM INTO(在线安全,含 WAL 内容);
// PG 档备份走 pg_dump / WAL 归档(运维路径,见 usage 指南),此处返回指引错误。
func (s *Store) Backup(destPath string) error {
if s.dialect.name() != "sqlite" {
return errors.New("store: PG 档备份走 pg_dump/WAL 归档(见 docs/guides/usage.md 存储档一节)")
}
if _, err := s.db.Exec(`VACUUM INTO ?`, destPath); err != nil {
return fmt.Errorf("store: backup: %w", err)
}
return nil
}
func nowISO() string { return time.Now().UTC().Format(time.RFC3339Nano) }
func marshalJSON(v any) string {
b, _ := json.Marshal(v)
return string(b)
}
func unmarshalJSON[T any](raw string) (T, error) {
var out T
err := json.Unmarshal([]byte(raw), &out)
return out, err
}