Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
126 lines
3.1 KiB
Go
126 lines
3.1 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (m *Memory) prunableRevisionIDsLocked(tenantID string, cutoff time.Time) []string {
|
|
var newest *Revision
|
|
for _, rev := range m.revisions {
|
|
if rev == nil || rev.TenantID != tenantID {
|
|
continue
|
|
}
|
|
if newest == nil || rev.CreatedAt.After(newest.CreatedAt) {
|
|
newest = rev
|
|
}
|
|
}
|
|
if newest == nil {
|
|
return nil
|
|
}
|
|
protected := map[string]struct{}{newest.ID: {}}
|
|
for _, sp := range m.speakers {
|
|
if sp == nil || sp.TenantID != tenantID {
|
|
continue
|
|
}
|
|
if sp.LastAppliedRevisionID != nil && strings.TrimSpace(*sp.LastAppliedRevisionID) != "" {
|
|
protected[strings.TrimSpace(*sp.LastAppliedRevisionID)] = struct{}{}
|
|
}
|
|
}
|
|
for speakerID, info := range m.publishedRevision {
|
|
sp, ok := m.speakers[speakerID]
|
|
if !ok || sp == nil || sp.TenantID != tenantID {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(info.RevisionID) != "" {
|
|
protected[strings.TrimSpace(info.RevisionID)] = struct{}{}
|
|
}
|
|
}
|
|
var ids []string
|
|
for id, rev := range m.revisions {
|
|
if rev == nil || rev.TenantID != tenantID {
|
|
continue
|
|
}
|
|
if !rev.CreatedAt.Before(cutoff) {
|
|
continue
|
|
}
|
|
if _, keep := protected[id]; keep {
|
|
continue
|
|
}
|
|
ids = append(ids, id)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func revisionBytesEstimate(rev *Revision, prefixes []PrefixRow) int64 {
|
|
if rev == nil {
|
|
return 0
|
|
}
|
|
var n int64
|
|
n += int64(len(rev.ContentHash))
|
|
for _, v := range rev.PreviewFragments {
|
|
n += int64(len(v))
|
|
}
|
|
for _, pr := range prefixes {
|
|
n += int64(len(pr.Prefix) + len(pr.Source))
|
|
}
|
|
return n
|
|
}
|
|
|
|
func (m *Memory) EstimateRevisionPrune(tenantID string, cutoff time.Time, retentionMinutes int) (RevisionPruneEstimate, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
ids := m.prunableRevisionIDsLocked(tenantID, cutoff)
|
|
var bytes int64
|
|
for _, id := range ids {
|
|
bytes += revisionBytesEstimate(m.revisions[id], m.revPrefixes[id])
|
|
}
|
|
prefixRows := 0
|
|
for _, id := range ids {
|
|
prefixRows += len(m.revPrefixes[id])
|
|
}
|
|
return RevisionPruneEstimate{
|
|
RetentionMinutes: retentionMinutes,
|
|
CutoffAt: cutoff.UTC(),
|
|
RevisionCount: len(ids),
|
|
PrefixRowCount: prefixRows,
|
|
OrphanSnapshotCount: 0,
|
|
BytesEstimate: bytes,
|
|
}, nil
|
|
}
|
|
|
|
func (m *Memory) PruneRevisionsWithStats(tenantID string, cutoff time.Time) (RevisionPruneResult, error) {
|
|
est, err := m.EstimateRevisionPrune(tenantID, cutoff, 0)
|
|
if err != nil {
|
|
return RevisionPruneResult{}, err
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
ids := m.prunableRevisionIDsLocked(tenantID, cutoff)
|
|
for _, id := range ids {
|
|
delete(m.revisions, id)
|
|
delete(m.revPrefixes, id)
|
|
}
|
|
return RevisionPruneResult{
|
|
DeletedRevisions: len(ids),
|
|
DeletedPrefixSnapshots: 0,
|
|
DeletedPrefixRows: est.PrefixRowCount,
|
|
BytesEstimate: est.BytesEstimate,
|
|
}, nil
|
|
}
|
|
|
|
func (m *Memory) PruneRevisionsBefore(ctx context.Context, tenantID string, cutoff time.Time) (int, error) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
res, err := m.PruneRevisionsWithStats(tenantID, cutoff)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return res.DeletedRevisions, nil
|
|
}
|