feat(revisions): add pruning estimate and cleanup endpoints
CI / changes (push) Successful in 8s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Successful in 32s
CI / go (push) Successful in 57s
CI / bird2 (push) Successful in 15s
CI / release (push) Successful in 3m18s
CI / changes (push) Successful in 8s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 25s
CI / web (push) Successful in 32s
CI / go (push) Successful in 57s
CI / bird2 (push) Successful in 15s
CI / release (push) Successful in 3m18s
Implemented new endpoints for estimating and pruning revisions, including detailed schemas for requests and responses. The `RevisionPruneEstimate` and `RevisionPruneResult` components were added to the OpenAPI documentation, enhancing the API's functionality for managing revision retention. Updated the backend to support these operations and integrated them into the tenant settings UI for improved user interaction.
This commit is contained in:
1 parent
5dbdac3d2c
commit
f39df7c4bf
18 files changed
+1146
-130
No files matched your search
@@ -84,6 +84,8 @@ type Backend interface {
|
||||
// CreateRenderRevision inserts a new config_revision (revID must be unique) with materialized prefixes and preview fragments.
|
||||
CreateRenderRevision(revisionID, tenantID, moduleID string, parentRevisionID *string, contentHash string, previewFragments map[string]string, prefixes []PrefixRow) error
|
||||
RevisionDiff(tenantID, aID, bID string) (map[string]any, error)
|
||||
EstimateRevisionPrune(tenantID string, cutoff time.Time, retentionMinutes int) (RevisionPruneEstimate, error)
|
||||
PruneRevisionsWithStats(tenantID string, cutoff time.Time) (RevisionPruneResult, error)
|
||||
PruneRevisionsBefore(tenantID string, cutoff time.Time) (deleted int, err error)
|
||||
SetLastAppliedRevision(tenantID, speakerID, revisionID string) error
|
||||
PublishRevisionForSpeaker(speakerID, revisionID string) error
|
||||
|
||||
@@ -616,62 +616,6 @@ func (m *Memory) RevisionDiff(tenantID, aID, bID string) (map[string]any, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *Memory) PruneRevisionsBefore(tenantID string, cutoff time.Time) (int, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
var newest *Revision
|
||||
for _, rev := range m.revisions {
|
||||
if rev.TenantID != tenantID {
|
||||
continue
|
||||
}
|
||||
if newest == nil || rev.CreatedAt.After(newest.CreatedAt) {
|
||||
newest = rev
|
||||
}
|
||||
}
|
||||
if newest == nil {
|
||||
return 0, 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{}{}
|
||||
}
|
||||
}
|
||||
|
||||
deleted := 0
|
||||
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
|
||||
}
|
||||
delete(m.revisions, id)
|
||||
delete(m.revPrefixes, id)
|
||||
deleted++
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
func (m *Memory) getRevisionLocked(tenantID, revisionID string) (*Revision, error) {
|
||||
rev, ok := m.revisions[revisionID]
|
||||
if !ok {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"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(tenantID string, cutoff time.Time) (int, error) {
|
||||
res, err := m.PruneRevisionsWithStats(tenantID, cutoff)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.DeletedRevisions, nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package store
|
||||
|
||||
import "time"
|
||||
|
||||
// RevisionPruneEstimate describes revisions and storage that would be removed by prune.
|
||||
type RevisionPruneEstimate struct {
|
||||
RetentionMinutes int `json:"retention_minutes"`
|
||||
CutoffAt time.Time `json:"cutoff_at"`
|
||||
RevisionCount int `json:"revision_count"`
|
||||
PrefixRowCount int `json:"prefix_row_count"`
|
||||
OrphanSnapshotCount int `json:"orphan_snapshot_count"`
|
||||
BytesEstimate int64 `json:"bytes_estimate"`
|
||||
}
|
||||
|
||||
// RevisionPruneResult is the outcome of a revision prune run.
|
||||
type RevisionPruneResult struct {
|
||||
DeletedRevisions int `json:"deleted_revisions"`
|
||||
DeletedPrefixSnapshots int `json:"deleted_prefix_snapshots"`
|
||||
DeletedPrefixRows int `json:"deleted_prefix_rows"`
|
||||
BytesEstimate int64 `json:"bytes_estimate"`
|
||||
}
|
||||
Reference in new issue
Block a user