feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
924f111e48
commit
f8e0520817
197 files changed
+18655
-1416
No files matched your search
@@ -0,0 +1,108 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"evobgp/internal/store"
|
||||
)
|
||||
|
||||
const (
|
||||
revisionPruneMinInterval = 5 * time.Minute
|
||||
revisionPruneTimeout = 2 * time.Minute
|
||||
)
|
||||
|
||||
// revisionPruneGate runs at most one prune per tenant per interval and drops
|
||||
// overlapping attempts so renders do not stack prune transactions.
|
||||
type revisionPruneGate struct {
|
||||
mu sync.Mutex
|
||||
last map[string]time.Time
|
||||
inflight map[string]chan struct{}
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
var revisionPrunes revisionPruneGate
|
||||
|
||||
func (g *revisionPruneGate) start(tenantID string) bool {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
now := time.Now()
|
||||
if g.now != nil {
|
||||
now = g.now()
|
||||
}
|
||||
if g.last == nil {
|
||||
g.last = make(map[string]time.Time)
|
||||
g.inflight = make(map[string]chan struct{})
|
||||
}
|
||||
if _, running := g.inflight[tenantID]; running {
|
||||
return false
|
||||
}
|
||||
if last, ok := g.last[tenantID]; ok && now.Sub(last) < revisionPruneMinInterval {
|
||||
return false
|
||||
}
|
||||
g.last[tenantID] = now
|
||||
g.inflight[tenantID] = make(chan struct{})
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *revisionPruneGate) done(tenantID string) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
ch, ok := g.inflight[tenantID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
close(ch)
|
||||
delete(g.inflight, tenantID)
|
||||
}
|
||||
|
||||
// applyRevisionRetention schedules retention for tenantID and returns without
|
||||
// waiting. Prune uses its own short context, at most once per tenant per five
|
||||
// minutes, and overlapping renders do not start another transaction.
|
||||
func applyRevisionRetention(st store.Backend, tenantID string) {
|
||||
tenantID = strings.TrimSpace(tenantID)
|
||||
if st == nil || tenantID == "" {
|
||||
return
|
||||
}
|
||||
if !revisionPrunes.start(tenantID) {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer revisionPrunes.done(tenantID)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), revisionPruneTimeout)
|
||||
defer cancel()
|
||||
pruneTenantRevisions(ctx, st, tenantID)
|
||||
}()
|
||||
}
|
||||
|
||||
// pruneTenantRevisions applies the tenant retention cutoff immediately.
|
||||
func pruneTenantRevisions(ctx context.Context, st store.Backend, tenantID string) {
|
||||
if st == nil || ctx == nil {
|
||||
return
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
settings, err := st.ListGlobalSettings(tenantID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, _ = st.PruneRevisionsBefore(ctx, tenantID, RevisionRetentionCutoff(settings))
|
||||
}
|
||||
|
||||
func waitRevisionPrune(ctx context.Context, tenantID string) error {
|
||||
revisionPrunes.mu.Lock()
|
||||
ch := revisionPrunes.inflight[strings.TrimSpace(tenantID)]
|
||||
revisionPrunes.mu.Unlock()
|
||||
if ch == nil {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ch:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user