Changeset 0.37.14 (#226)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
124
server/retention/scanner.go
Normal file
124
server/retention/scanner.go
Normal file
@@ -0,0 +1,124 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user