This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/retention/scanner.go
gobha b7746c3004 Changeset 0.37.14 (#226)
Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
2026-03-23 16:47:48 +00:00

125 lines
2.6 KiB
Go

package retention
import (
"context"
"fmt"
"log"
"sync"
"time"
"chat-switchboard/storage"
"chat-switchboard/store"
)
const DefaultInterval = 1 * time.Hour
// ScannerConfig holds startup configuration.
type ScannerConfig struct {
Interval time.Duration
}
// Scanner periodically purges channels whose purge_after timestamp has passed.
// Channels with global/team provider scopes are archived with a TTL by
// DeleteChannel; this scanner performs the deferred hard-delete.
type Scanner struct {
stores store.Stores
objStore storage.ObjectStore
wg sync.WaitGroup
stopCh chan struct{}
interval time.Duration
}
// NewScanner creates a retention scanner. Call Start() to begin.
func NewScanner(stores store.Stores, objStore storage.ObjectStore, cfg ScannerConfig) *Scanner {
interval := cfg.Interval
if interval <= 0 {
interval = DefaultInterval
}
return &Scanner{
stores: stores,
objStore: objStore,
stopCh: make(chan struct{}),
interval: interval,
}
}
// Start begins the scan loop in a background goroutine.
func (sc *Scanner) Start() {
sc.wg.Add(1)
go func() {
defer sc.wg.Done()
sc.loop()
}()
log.Printf("[retention] scanner started (interval=%s)", sc.interval)
}
// Stop signals the scanner to stop and waits for in-flight work to drain.
func (sc *Scanner) Stop() {
close(sc.stopCh)
sc.wg.Wait()
log.Printf("[retention] scanner stopped")
}
func (sc *Scanner) loop() {
ticker := time.NewTicker(sc.interval)
defer ticker.Stop()
for {
select {
case <-sc.stopCh:
return
case <-ticker.C:
sc.tick()
}
}
}
func (sc *Scanner) tick() {
ctx := context.Background()
// If TTL is 0 the feature is disabled — nothing to purge
ttl := sc.retentionTTL(ctx)
if ttl <= 0 {
return
}
ids, err := sc.stores.Channels.ListPurgeable(ctx)
if err != nil {
log.Printf("[retention] ListPurgeable error: %v", err)
return
}
if len(ids) == 0 {
return
}
log.Printf("[retention] purging %d channel(s)", len(ids))
for _, id := range ids {
// Clean up storage files first
if sc.objStore != nil {
prefix := fmt.Sprintf("files/%s", id)
if err := sc.objStore.DeletePrefix(ctx, prefix); err != nil {
log.Printf("[retention] storage cleanup for %s failed: %v", id, err)
}
}
// Hard delete (Purge verifies is_archived)
if err := sc.stores.Channels.Purge(ctx, id); err != nil {
log.Printf("[retention] purge %s failed: %v", id, err)
}
}
}
func (sc *Scanner) retentionTTL(ctx context.Context) int {
cfg, err := sc.stores.GlobalConfig.Get(ctx, "retention_ttl_days")
if err != nil {
return 0
}
if v, ok := cfg["value"].(float64); ok {
return int(v)
}
return 0
}