Files
weichenandCursor be0c1e843f feat(dataset): registry, pause/resume backfill, destroy gates, eight tracks
批准 dataset 分类入库;SKIP LOCKED 分批;销毁默认 fail closed。restore 演练拒绝共享 env DSN。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 02:40:12 +00:00

183 lines
4.7 KiB
Go

package dataset
import (
"context"
"fmt"
"sync"
"chat-agent-system/internal/artifactvault"
)
type Enumerator interface {
Next(ctx context.Context, kind string, lastPK int64, limit int) ([]artifactvault.Candidate, error)
}
type MemoryEnumerator struct {
mu sync.Mutex
items map[string][]artifactvault.Candidate
}
func NewMemoryEnumerator() *MemoryEnumerator {
return &MemoryEnumerator{items: map[string][]artifactvault.Candidate{}}
}
func (m *MemoryEnumerator) Seed(kind string, items []artifactvault.Candidate) {
m.mu.Lock()
defer m.mu.Unlock()
cp := append([]artifactvault.Candidate(nil), items...)
m.items[kind] = cp
}
func (m *MemoryEnumerator) Next(_ context.Context, kind string, lastPK int64, limit int) ([]artifactvault.Candidate, error) {
m.mu.Lock()
defer m.mu.Unlock()
all := m.items[kind]
out := make([]artifactvault.Candidate, 0, limit)
for _, c := range all {
if c.SourcePK <= lastPK {
continue
}
out = append(out, c)
if limit > 0 && len(out) >= limit {
break
}
}
return out, nil
}
type Job struct {
ID string
Status string
SourceSchema int
SourceCommit string
StorageIdentity string
Checkpoints map[string]Checkpoint
}
func NewJob(id, commit, identity string, schema int) *Job {
return &Job{
ID: id, Status: JobRunning, SourceSchema: schema, SourceCommit: commit,
StorageIdentity: identity, Checkpoints: map[string]Checkpoint{},
}
}
func (j *Job) Pause() {
if j.Status == JobRunning {
j.Status = JobPaused
}
}
func (j *Job) Resume() error {
if j.Status != JobPaused && j.Status != JobRunning {
return fmt.Errorf("dataset: job %s cannot resume from %s", j.ID, j.Status)
}
j.Status = JobRunning
return nil
}
type Runner struct {
Enum Enumerator
Env string
Tx artifactvaultTx
}
type artifactvaultTx interface {
Process(ctx context.Context, env string, c artifactvault.Candidate) (artifactvault.ProcessResult, error)
}
type memoryProcessor struct {
reg *artifactvault.MemoryRegistry
}
func NewMemoryRunner(enum Enumerator, reg *artifactvault.MemoryRegistry) *Runner {
if reg == nil {
reg = artifactvault.NewMemoryRegistry()
}
return &Runner{Enum: enum, Env: "dev", Tx: &memoryProcessor{reg: reg}}
}
func (p *memoryProcessor) Process(ctx context.Context, env string, c artifactvault.Candidate) (artifactvault.ProcessResult, error) {
if hz := artifactvault.ClassifyHazard(c); hz != artifactvault.HazardNone {
_ = p.reg.Quarantine(ctx, hz, c.LogicalID)
return artifactvault.ProcessResult{Status: "quarantine", Hazard: hz}, nil
}
if c.Kind == artifactvault.KindSnapshot {
return artifactvault.ProcessResult{Status: "done"}, nil
}
ing := &artifactvault.Ingest{Env: env, Reg: p.reg}
var err error
switch c.Kind {
case artifactvault.KindAgentCommit:
err = ing.AgentCommit(ctx, c.ProjectID, c.LogicalID, nz(c.EventID, fmt.Sprintf("commit:%d", c.SourcePK)), c.SnapshotID, c.Payload)
case artifactvault.KindApproved:
err = ing.Approved(ctx, c.ProjectID, c.LogicalID, c.SnapshotID, c.ApprovedAt, c.Payload)
case artifactvault.KindChatTurn:
policy := c.ExportPolicy
if policy == "" {
policy = "full"
}
err = ing.AssistantReply(ctx, c.ProjectID, c.LogicalID, nz(c.EventID, fmt.Sprintf("turn:%d", c.SourcePK)), c.MessageID, c.RunID, policy, c.CompletedAt, c.Payload)
default:
return artifactvault.ProcessResult{}, fmt.Errorf("dataset: kind %s", c.Kind)
}
if err != nil {
return artifactvault.ProcessResult{}, err
}
return artifactvault.ProcessResult{Status: "done"}, nil
}
func nz(v, def string) string {
if v == "" {
return def
}
return v
}
func (r *Runner) RunBatch(ctx context.Context, job *Job, kind string, shard, limit int) (JobStats, error) {
if job.Status == JobPaused {
return Recalc(checkpointsOf(job)), fmt.Errorf("dataset: job paused")
}
if job.Status != JobRunning {
return JobStats{}, fmt.Errorf("dataset: job not running")
}
key := fmt.Sprintf("%s:%d", kind, shard)
cp := job.Checkpoints[key]
cp.JobID, cp.Shard, cp.SourceKind = job.ID, shard, kind
items, err := r.Enum.Next(ctx, kind, cp.LastPK, limit)
if err != nil {
job.Status = JobFailed
return Recalc(checkpointsOf(job)), err
}
env := r.Env
if env == "" {
env = "dev"
}
for _, c := range items {
c.SourceSchema = job.SourceSchema
res, err := r.Tx.Process(ctx, env, c)
cp.Scanned++
cp.LastPK = c.SourcePK
if err != nil {
job.Checkpoints[key] = cp
job.Status = JobFailed
return Recalc(checkpointsOf(job)), err
}
switch res.Status {
case "quarantine":
cp.Quarantined++
default:
cp.Succeeded++
}
}
job.Checkpoints[key] = cp
return Recalc(checkpointsOf(job)), nil
}
func checkpointsOf(job *Job) []Checkpoint {
out := make([]Checkpoint, 0, len(job.Checkpoints))
for _, c := range job.Checkpoints {
out = append(out, c)
}
return out
}