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
This commit is contained in:
+11
-27
@@ -5,8 +5,6 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
core "agent-system-core"
|
||||
@@ -16,18 +14,6 @@ import (
|
||||
// 文件写走 临时文件 → rename → 落库:崩溃残留只可能产生无主孤立文件,
|
||||
// 不产生库里有行、对象缺失的悬空引用。
|
||||
|
||||
func (s *Store) writeBlobAtomic(path string, content []byte) error {
|
||||
tmp := path + ".tmp-" + newID("x")
|
||||
if err := os.WriteFile(tmp, content, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmp, path); err != nil {
|
||||
os.Remove(tmp)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateArtifact(a *core.Artifact, content []byte) error {
|
||||
if a.ID == "" {
|
||||
a.ID = newID("art")
|
||||
@@ -35,14 +21,14 @@ func (s *Store) CreateArtifact(a *core.Artifact, content []byte) error {
|
||||
now := time.Now().UTC()
|
||||
a.CreatedAt, a.UpdatedAt = now, now
|
||||
a.Size = int64(len(content))
|
||||
path := filepath.Join(s.dataDir, a.ID)
|
||||
if err := s.writeBlobAtomic(path, content); err != nil {
|
||||
ref, err := s.blobs.Put(a.ID, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := s.db.Exec(s.q(`INSERT INTO artifacts(id,project_id,owner,name,kind,mime,size,source_run,content_path,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)`),
|
||||
a.ID, a.ProjectID, a.Owner, a.Name, a.Kind, a.MIME, a.Size, a.SourceRun, path, nowISO(), nowISO())
|
||||
_, err = s.db.Exec(s.q(`INSERT INTO artifacts(id,project_id,owner,name,kind,mime,size,source_run,content_path,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)`),
|
||||
a.ID, a.ProjectID, a.Owner, a.Name, a.Kind, a.MIME, a.Size, a.SourceRun, ref, nowISO(), nowISO())
|
||||
if err != nil {
|
||||
os.Remove(path) // 落库失败:回收对象,不留悬空
|
||||
_ = s.blobs.Delete(ref) // 落库失败:回收对象,不留悬空
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -57,7 +43,8 @@ func (s *Store) UpdateArtifactContent(owner, id string, content []byte) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.writeBlobAtomic(path, content); err != nil {
|
||||
// 同 ID 原子覆写(临时文件+rename);行内引用不变,避免行/对象漂移。
|
||||
if _, err := s.blobs.Put(id, content); err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := s.db.Exec(s.q(`UPDATE artifacts SET size=?, updated_at=? WHERE id=? AND owner=?`), len(content), nowISO(), id, owner)
|
||||
@@ -96,15 +83,15 @@ func (s *Store) GetArtifact(owner, id string) (core.Artifact, error) {
|
||||
}
|
||||
|
||||
func (s *Store) ArtifactContent(owner, id string) ([]byte, error) {
|
||||
var path string
|
||||
err := s.db.QueryRow(s.q(`SELECT content_path FROM artifacts WHERE id=? AND owner=?`), id, owner).Scan(&path)
|
||||
var ref string
|
||||
err := s.db.QueryRow(s.q(`SELECT content_path FROM artifacts WHERE id=? AND owner=?`), id, owner).Scan(&ref)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.ReadFile(path)
|
||||
return s.blobs.Get(ref)
|
||||
}
|
||||
|
||||
func (s *Store) ListArtifacts(owner, projectID string) ([]core.Artifact, error) {
|
||||
@@ -140,10 +127,7 @@ func (s *Store) DeleteArtifact(owner, id string) error {
|
||||
// 联动清理检索索引(知识行 + 向量;失败不阻断删除,残留可由重建索引收敛)。
|
||||
_ = s.DeleteKnowledgeByRef(owner, id)
|
||||
_ = s.DeleteKnowledgeVectors(context.Background(), owner, id)
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return s.blobs.Delete(path)
|
||||
}
|
||||
|
||||
// ---- HITL 卡片(resolve 原子化:条件更新消灭先查后改窗口)----
|
||||
|
||||
Reference in New Issue
Block a user