2026-05-20 10:46:49 +08:00
|
|
|
package scheduler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
defaultMonitoringRetentionDays = 30
|
|
|
|
|
defaultMonitoringRetentionBatch = 5000
|
|
|
|
|
defaultMonitoringRetentionBatches = 200
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type MonitoringRetentionWorker struct {
|
|
|
|
|
monitoringPool *pgxpool.Pool
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type retentionTarget struct {
|
|
|
|
|
Name string
|
|
|
|
|
DeleteSQL string
|
2026-05-20 12:19:27 +08:00
|
|
|
ProbeSQL string
|
2026-05-20 10:46:49 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewMonitoringRetentionWorker(monitoringPool *pgxpool.Pool, logger *zap.Logger) *MonitoringRetentionWorker {
|
|
|
|
|
return &MonitoringRetentionWorker{monitoringPool: monitoringPool, logger: logger}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *MonitoringRetentionWorker) RunOnce(ctx context.Context, run JobRunContext) (map[string]any, error) {
|
|
|
|
|
if w == nil || w.monitoringPool == nil {
|
|
|
|
|
return map[string]any{"skipped": true, "reason": "worker_not_ready"}, nil
|
|
|
|
|
}
|
|
|
|
|
startedAt := time.Now()
|
|
|
|
|
retentionDays := AsInt(run.Config, "retention_days", defaultMonitoringRetentionDays)
|
|
|
|
|
if retentionDays < defaultMonitoringRetentionDays {
|
|
|
|
|
retentionDays = defaultMonitoringRetentionDays
|
|
|
|
|
}
|
|
|
|
|
batchSize := AsInt(run.Config, "batch_size", defaultMonitoringRetentionBatch)
|
|
|
|
|
if run.Job != nil && run.Job.BatchSize != nil {
|
|
|
|
|
batchSize = *run.Job.BatchSize
|
|
|
|
|
}
|
|
|
|
|
if batchSize <= 0 {
|
|
|
|
|
batchSize = defaultMonitoringRetentionBatch
|
|
|
|
|
}
|
|
|
|
|
maxBatches := AsInt(run.Config, "max_batches_per_run", defaultMonitoringRetentionBatches)
|
|
|
|
|
if maxBatches <= 0 {
|
|
|
|
|
maxBatches = defaultMonitoringRetentionBatches
|
|
|
|
|
}
|
|
|
|
|
statementTimeout := AsDuration(run.Config, "statement_timeout", 5*time.Second)
|
|
|
|
|
cutoffDate := monitoringRetentionCutoffDate(time.Now(), retentionDays)
|
|
|
|
|
dryRun := run.DryRun
|
|
|
|
|
|
|
|
|
|
stats := map[string]any{
|
|
|
|
|
"retention_days": retentionDays,
|
|
|
|
|
"cutoff_date": cutoffDate,
|
|
|
|
|
"batch_size": batchSize,
|
|
|
|
|
"max_batches_per_run": maxBatches,
|
|
|
|
|
"dry_run": dryRun,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conn, err := w.monitoringPool.Acquire(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return stats, fmt.Errorf("acquire monitoring connection: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer conn.Release()
|
|
|
|
|
|
|
|
|
|
if statementTimeout > 0 {
|
2026-05-20 11:22:37 +08:00
|
|
|
if _, err := conn.Exec(ctx, `SELECT set_config('statement_timeout', $1, false)`, fmt.Sprintf("%dms", statementTimeout.Milliseconds())); err != nil {
|
2026-05-20 10:46:49 +08:00
|
|
|
return stats, fmt.Errorf("set statement timeout: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer conn.Exec(context.Background(), `RESET statement_timeout`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
totalDeleted := int64(0)
|
2026-05-20 12:19:27 +08:00
|
|
|
totalCandidateProbe := int64(0)
|
2026-05-20 10:46:49 +08:00
|
|
|
perTable := map[string]any{}
|
|
|
|
|
for _, target := range retentionTargets() {
|
2026-05-20 12:19:27 +08:00
|
|
|
tableStats := map[string]any{}
|
|
|
|
|
if dryRun {
|
|
|
|
|
candidateProbe, err := w.probeTarget(ctx, conn, target, cutoffDate, batchSize)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return stats, fmt.Errorf("probe %s: %w", target.Name, err)
|
|
|
|
|
}
|
|
|
|
|
tableStats["candidate_probe_count"] = candidateProbe
|
|
|
|
|
tableStats["candidate_probe_limited"] = candidateProbe >= int64(batchSize)
|
|
|
|
|
totalCandidateProbe += candidateProbe
|
|
|
|
|
} else {
|
2026-05-20 10:46:49 +08:00
|
|
|
deleted, batches, err := w.deleteTarget(ctx, conn, target, cutoffDate, batchSize, maxBatches)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return stats, fmt.Errorf("delete %s: %w", target.Name, err)
|
|
|
|
|
}
|
|
|
|
|
tableStats["deleted_count"] = deleted
|
|
|
|
|
tableStats["batch_count"] = batches
|
|
|
|
|
totalDeleted += deleted
|
|
|
|
|
}
|
|
|
|
|
perTable[target.Name] = tableStats
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 12:19:27 +08:00
|
|
|
if dryRun {
|
|
|
|
|
stats["candidate_probe_count"] = totalCandidateProbe
|
|
|
|
|
}
|
2026-05-20 10:46:49 +08:00
|
|
|
stats["deleted_count"] = totalDeleted
|
|
|
|
|
stats["tables"] = perTable
|
|
|
|
|
stats["duration_ms"] = time.Since(startedAt).Milliseconds()
|
|
|
|
|
return stats, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func monitoringRetentionCutoffDate(now time.Time, retentionDays int) string {
|
|
|
|
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
|
|
|
|
if err != nil {
|
|
|
|
|
loc = time.FixedZone("UTC+8", 8*60*60)
|
|
|
|
|
}
|
|
|
|
|
localNow := now.In(loc)
|
|
|
|
|
today := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), 0, 0, 0, 0, loc)
|
|
|
|
|
cutoff := today.AddDate(0, 0, -(retentionDays - 1))
|
|
|
|
|
return cutoff.Format("2006-01-02")
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 12:19:27 +08:00
|
|
|
func (w *MonitoringRetentionWorker) probeTarget(ctx context.Context, conn *pgxpool.Conn, target retentionTarget, cutoffDate string, limit int) (int64, error) {
|
2026-05-20 10:46:49 +08:00
|
|
|
var count int64
|
2026-05-20 12:19:27 +08:00
|
|
|
if limit <= 0 {
|
|
|
|
|
limit = defaultMonitoringRetentionBatch
|
|
|
|
|
}
|
|
|
|
|
if err := conn.QueryRow(ctx, target.ProbeSQL, cutoffDate, limit).Scan(&count); err != nil {
|
2026-05-20 10:46:49 +08:00
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
return count, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (w *MonitoringRetentionWorker) deleteTarget(ctx context.Context, conn *pgxpool.Conn, target retentionTarget, cutoffDate string, batchSize, maxBatches int) (int64, int, error) {
|
|
|
|
|
total := int64(0)
|
|
|
|
|
batches := 0
|
|
|
|
|
for batches < maxBatches {
|
|
|
|
|
tag, err := conn.Exec(ctx, target.DeleteSQL, cutoffDate, batchSize)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return total, batches, err
|
|
|
|
|
}
|
|
|
|
|
affected := tag.RowsAffected()
|
|
|
|
|
if affected == 0 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
total += affected
|
|
|
|
|
batches++
|
|
|
|
|
if affected < int64(batchSize) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return total, batches, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func retentionTargets() []retentionTarget {
|
|
|
|
|
return []retentionTarget{
|
|
|
|
|
{
|
|
|
|
|
Name: "monitoring_collect_dispatch_outbox",
|
2026-05-20 12:19:27 +08:00
|
|
|
ProbeSQL: batchProbeSQL("monitoring_collect_dispatch_outbox", "id", "created_at < $1::date", "created_at ASC, id ASC"),
|
|
|
|
|
DeleteSQL: batchDeleteSQL("monitoring_collect_dispatch_outbox", "id", "created_at < $1::date", "created_at ASC, id ASC"),
|
2026-05-20 10:46:49 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "monitoring_collect_requests",
|
2026-05-20 12:19:27 +08:00
|
|
|
ProbeSQL: batchProbeSQL("monitoring_collect_requests", "id", "created_at < $1::date", "created_at ASC, id ASC"),
|
|
|
|
|
DeleteSQL: batchDeleteSQL("monitoring_collect_requests", "id", "created_at < $1::date", "created_at ASC, id ASC"),
|
2026-05-20 10:46:49 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "monitoring_brand_platform_daily",
|
2026-05-20 12:19:27 +08:00
|
|
|
ProbeSQL: batchProbeSQL("monitoring_brand_platform_daily", "id", "business_date < $1::date", "business_date ASC, id ASC"),
|
|
|
|
|
DeleteSQL: batchDeleteSQL("monitoring_brand_platform_daily", "id", "business_date < $1::date", "business_date ASC, id ASC"),
|
2026-05-20 10:46:49 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "monitoring_brand_daily",
|
2026-05-20 12:19:27 +08:00
|
|
|
ProbeSQL: batchProbeSQL("monitoring_brand_daily", "id", "business_date < $1::date", "business_date ASC, id ASC"),
|
|
|
|
|
DeleteSQL: batchDeleteSQL("monitoring_brand_daily", "id", "business_date < $1::date", "business_date ASC, id ASC"),
|
2026-05-20 10:46:49 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "monitoring_platform_access_snapshots",
|
2026-05-20 12:19:27 +08:00
|
|
|
ProbeSQL: batchProbeSQL("monitoring_platform_access_snapshots", "id", "business_date < $1::date", "business_date ASC, id ASC"),
|
|
|
|
|
DeleteSQL: batchDeleteSQL("monitoring_platform_access_snapshots", "id", "business_date < $1::date", "business_date ASC, id ASC"),
|
2026-05-20 10:46:49 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "monitoring_collect_tasks",
|
2026-05-20 12:19:27 +08:00
|
|
|
ProbeSQL: batchProbeSQL("monitoring_collect_tasks", "id", "business_date < $1::date", "business_date ASC, id ASC"),
|
|
|
|
|
DeleteSQL: batchDeleteSQL("monitoring_collect_tasks", "id", "business_date < $1::date", "business_date ASC, id ASC"),
|
2026-05-20 10:46:49 +08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Name: "question_monitor_runs",
|
2026-05-20 12:19:27 +08:00
|
|
|
ProbeSQL: batchProbeSQL("question_monitor_runs", "id", "business_date < $1::date", "business_date ASC, id ASC"),
|
|
|
|
|
DeleteSQL: batchDeleteSQL("question_monitor_runs", "id", "business_date < $1::date", "business_date ASC, id ASC"),
|
2026-05-20 10:46:49 +08:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 12:19:27 +08:00
|
|
|
func batchProbeSQL(table, idColumn, predicate, orderBy string) string {
|
|
|
|
|
parts := []string{
|
|
|
|
|
"SELECT COUNT(*)::bigint",
|
|
|
|
|
"FROM (",
|
|
|
|
|
"SELECT " + idColumn,
|
|
|
|
|
"FROM " + table,
|
|
|
|
|
"WHERE " + predicate,
|
|
|
|
|
"ORDER BY " + orderBy,
|
|
|
|
|
"LIMIT $2",
|
|
|
|
|
") candidates",
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(parts, "\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func batchDeleteSQL(table, idColumn, predicate, orderBy string) string {
|
2026-05-20 10:46:49 +08:00
|
|
|
parts := []string{
|
|
|
|
|
"WITH doomed AS (",
|
|
|
|
|
"SELECT " + idColumn,
|
|
|
|
|
"FROM " + table,
|
|
|
|
|
"WHERE " + predicate,
|
2026-05-20 12:19:27 +08:00
|
|
|
"ORDER BY " + orderBy,
|
2026-05-20 10:46:49 +08:00
|
|
|
"LIMIT $2",
|
2026-05-20 12:19:27 +08:00
|
|
|
"FOR UPDATE SKIP LOCKED",
|
2026-05-20 10:46:49 +08:00
|
|
|
")",
|
2026-05-20 12:19:27 +08:00
|
|
|
"DELETE FROM " + table + " t",
|
|
|
|
|
"USING doomed",
|
|
|
|
|
"WHERE t." + idColumn + " = doomed." + idColumn,
|
2026-05-20 10:46:49 +08:00
|
|
|
}
|
|
|
|
|
return strings.Join(parts, "\n")
|
|
|
|
|
}
|