162 lines
5.1 KiB
Go
162 lines
5.1 KiB
Go
package worker
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
)
|
|
|
|
// SQLRetentionStore performs the durable DELETE statements applied by the
|
|
// retention worker. Implementations are typically the umbrella PostgreSQL
|
|
// notification store; the interface keeps the worker decoupled from the
|
|
// store package.
|
|
type SQLRetentionStore interface {
|
|
// DeleteRecordsOlderThan removes records rows whose accepted_at predates
|
|
// cutoff. Cascading FKs drop routes and dead_letters owned by the deleted
|
|
// rows.
|
|
DeleteRecordsOlderThan(ctx context.Context, cutoff time.Time) (int64, error)
|
|
|
|
// DeleteMalformedIntentsOlderThan removes malformed-intent rows whose
|
|
// recorded_at predates cutoff.
|
|
DeleteMalformedIntentsOlderThan(ctx context.Context, cutoff time.Time) (int64, error)
|
|
}
|
|
|
|
// SQLRetentionConfig stores the dependencies and policy used by
|
|
// SQLRetentionWorker.
|
|
type SQLRetentionConfig struct {
|
|
// Store applies the durable DELETE statements.
|
|
Store SQLRetentionStore
|
|
|
|
// RecordRetention bounds how long records (and their cascaded routes and
|
|
// dead_letters) survive after acceptance.
|
|
RecordRetention time.Duration
|
|
|
|
// MalformedIntentRetention bounds how long malformed-intent rows survive
|
|
// after recorded_at.
|
|
MalformedIntentRetention time.Duration
|
|
|
|
// CleanupInterval stores the wall-clock period between two retention
|
|
// passes.
|
|
CleanupInterval time.Duration
|
|
|
|
// Clock provides the wall-clock used to compute cutoff timestamps.
|
|
Clock Clock
|
|
}
|
|
|
|
// SQLRetentionWorker periodically deletes records and malformed-intent rows
|
|
// whose retention window has expired. The worker replaces the per-key
|
|
// Redis EXPIRE eviction that maintained TTLs on the previous Redis-backed
|
|
// notification keyspace.
|
|
type SQLRetentionWorker struct {
|
|
store SQLRetentionStore
|
|
recordRetention time.Duration
|
|
malformedIntentRetention time.Duration
|
|
cleanupInterval time.Duration
|
|
clock Clock
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewSQLRetentionWorker constructs the periodic retention worker.
|
|
func NewSQLRetentionWorker(cfg SQLRetentionConfig, logger *slog.Logger) (*SQLRetentionWorker, error) {
|
|
switch {
|
|
case cfg.Store == nil:
|
|
return nil, errors.New("new sql retention worker: nil store")
|
|
case cfg.RecordRetention <= 0:
|
|
return nil, errors.New("new sql retention worker: non-positive record retention")
|
|
case cfg.MalformedIntentRetention <= 0:
|
|
return nil, errors.New("new sql retention worker: non-positive malformed intent retention")
|
|
case cfg.CleanupInterval <= 0:
|
|
return nil, errors.New("new sql retention worker: non-positive cleanup interval")
|
|
case cfg.Clock == nil:
|
|
return nil, errors.New("new sql retention worker: nil clock")
|
|
}
|
|
if logger == nil {
|
|
logger = slog.Default()
|
|
}
|
|
|
|
return &SQLRetentionWorker{
|
|
store: cfg.Store,
|
|
recordRetention: cfg.RecordRetention,
|
|
malformedIntentRetention: cfg.MalformedIntentRetention,
|
|
cleanupInterval: cfg.CleanupInterval,
|
|
clock: cfg.Clock,
|
|
logger: logger.With("component", "sql_retention_worker"),
|
|
}, nil
|
|
}
|
|
|
|
// Run starts the retention loop and blocks until ctx is canceled.
|
|
func (worker *SQLRetentionWorker) Run(ctx context.Context) error {
|
|
if ctx == nil {
|
|
return errors.New("run sql retention worker: nil context")
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if worker == nil {
|
|
return errors.New("run sql retention worker: nil worker")
|
|
}
|
|
|
|
worker.logger.Info("sql retention worker started",
|
|
"record_retention", worker.recordRetention.String(),
|
|
"malformed_intent_retention", worker.malformedIntentRetention.String(),
|
|
"cleanup_interval", worker.cleanupInterval.String(),
|
|
)
|
|
defer worker.logger.Info("sql retention worker stopped")
|
|
|
|
// First pass runs immediately so a freshly started service does not wait
|
|
// one full interval before evicting stale rows.
|
|
worker.runOnce(ctx)
|
|
|
|
ticker := time.NewTicker(worker.cleanupInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-ticker.C:
|
|
worker.runOnce(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shutdown stops the retention worker within ctx.
|
|
func (worker *SQLRetentionWorker) Shutdown(ctx context.Context) error {
|
|
if ctx == nil {
|
|
return errors.New("shutdown sql retention worker: nil context")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (worker *SQLRetentionWorker) runOnce(ctx context.Context) {
|
|
now := worker.clock.Now().UTC()
|
|
|
|
recordCutoff := now.Add(-worker.recordRetention)
|
|
if deleted, err := worker.store.DeleteRecordsOlderThan(ctx, recordCutoff); err != nil {
|
|
worker.logger.Warn("delete expired records failed",
|
|
"cutoff", recordCutoff,
|
|
"error", fmt.Sprintf("%v", err),
|
|
)
|
|
} else if deleted > 0 {
|
|
worker.logger.Info("expired records deleted",
|
|
"cutoff", recordCutoff,
|
|
"deleted", deleted,
|
|
)
|
|
}
|
|
|
|
malformedCutoff := now.Add(-worker.malformedIntentRetention)
|
|
if deleted, err := worker.store.DeleteMalformedIntentsOlderThan(ctx, malformedCutoff); err != nil {
|
|
worker.logger.Warn("delete expired malformed intents failed",
|
|
"cutoff", malformedCutoff,
|
|
"error", fmt.Sprintf("%v", err),
|
|
)
|
|
} else if deleted > 0 {
|
|
worker.logger.Info("expired malformed intents deleted",
|
|
"cutoff", malformedCutoff,
|
|
"deleted", deleted,
|
|
)
|
|
}
|
|
}
|