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() } }