ops_cleanup_executor.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. package service
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "strings"
  7. "time"
  8. )
  9. const (
  10. opsCleanupDefaultSchedule = "0 2 * * *"
  11. opsCleanupBatchSize = 5000
  12. opsCleanupCronStopTimeout = 3 * time.Second
  13. opsCleanupRunTimeout = 30 * time.Minute
  14. opsCleanupHeartbeatTimeout = 2 * time.Second
  15. )
  16. type opsCleanupTarget struct {
  17. retentionDays int
  18. table string
  19. timeCol string
  20. castDate bool
  21. counter *int64
  22. }
  23. type opsCleanupDeletedCounts struct {
  24. errorLogs int64
  25. ingressRejects int64
  26. alertEvents int64
  27. systemLogs int64
  28. logAudits int64
  29. systemMetrics int64
  30. hourlyPreagg int64
  31. dailyPreagg int64
  32. }
  33. func (c opsCleanupDeletedCounts) String() string {
  34. return fmt.Sprintf(
  35. "error_logs=%d ingress_rejects=%d alert_events=%d system_logs=%d log_audits=%d system_metrics=%d hourly_preagg=%d daily_preagg=%d",
  36. c.errorLogs,
  37. c.ingressRejects,
  38. c.alertEvents,
  39. c.systemLogs,
  40. c.logAudits,
  41. c.systemMetrics,
  42. c.hourlyPreagg,
  43. c.dailyPreagg,
  44. )
  45. }
  46. // opsCleanupPlan 把"保留天数"翻译成具体的清理动作。
  47. // - days < 0 → 跳过该项清理(ok=false),保留兼容老数据
  48. // - days == 0 → TRUNCATE TABLE(O(1) 全清),truncate=true
  49. // - days > 0 → 批量 DELETE 早于 now-N天 的行,cutoff = now - N 天
  50. func opsCleanupPlan(now time.Time, days int) (cutoff time.Time, truncate, ok bool) {
  51. if days < 0 {
  52. return time.Time{}, false, false
  53. }
  54. if days == 0 {
  55. return time.Time{}, true, true
  56. }
  57. return now.AddDate(0, 0, -days), false, true
  58. }
  59. func opsCleanupRunOne(
  60. ctx context.Context,
  61. db *sql.DB,
  62. truncate bool,
  63. cutoff time.Time,
  64. table, timeCol string,
  65. castDate bool,
  66. batchSize int,
  67. ) (int64, error) {
  68. if truncate {
  69. return truncateOpsTable(ctx, db, table)
  70. }
  71. return deleteOldRowsByID(ctx, db, table, timeCol, cutoff, batchSize, castDate)
  72. }
  73. func deleteOldRowsByID(
  74. ctx context.Context,
  75. db *sql.DB,
  76. table string,
  77. timeColumn string,
  78. cutoff time.Time,
  79. batchSize int,
  80. castCutoffToDate bool,
  81. ) (int64, error) {
  82. if db == nil {
  83. return 0, nil
  84. }
  85. if batchSize <= 0 {
  86. batchSize = opsCleanupBatchSize
  87. }
  88. where := fmt.Sprintf("%s < $1", timeColumn)
  89. if castCutoffToDate {
  90. where = fmt.Sprintf("%s < $1::date", timeColumn)
  91. }
  92. q := fmt.Sprintf(`
  93. WITH batch AS (
  94. SELECT id FROM %s
  95. WHERE %s
  96. ORDER BY id
  97. LIMIT $2
  98. )
  99. DELETE FROM %s
  100. WHERE id IN (SELECT id FROM batch)
  101. `, table, where, table)
  102. var total int64
  103. for {
  104. res, err := db.ExecContext(ctx, q, cutoff, batchSize)
  105. if err != nil {
  106. if isMissingRelationError(err) {
  107. return total, nil
  108. }
  109. return total, err
  110. }
  111. affected, err := res.RowsAffected()
  112. if err != nil {
  113. return total, err
  114. }
  115. total += affected
  116. if affected == 0 {
  117. break
  118. }
  119. }
  120. return total, nil
  121. }
  122. // truncateOpsTable 用 TRUNCATE TABLE 清空指定表,先 SELECT COUNT(*) 取得清空前行数用于 heartbeat。
  123. func truncateOpsTable(ctx context.Context, db *sql.DB, table string) (int64, error) {
  124. if db == nil {
  125. return 0, nil
  126. }
  127. var count int64
  128. if err := db.QueryRowContext(ctx, fmt.Sprintf("SELECT COUNT(*) FROM %s", table)).Scan(&count); err != nil {
  129. if isMissingRelationError(err) {
  130. return 0, nil
  131. }
  132. return 0, fmt.Errorf("count %s: %w", table, err)
  133. }
  134. if count == 0 {
  135. return 0, nil
  136. }
  137. if _, err := db.ExecContext(ctx, fmt.Sprintf("TRUNCATE TABLE %s", table)); err != nil {
  138. if isMissingRelationError(err) {
  139. return 0, nil
  140. }
  141. return 0, fmt.Errorf("truncate %s: %w", table, err)
  142. }
  143. return count, nil
  144. }
  145. func isMissingRelationError(err error) bool {
  146. if err == nil {
  147. return false
  148. }
  149. s := strings.ToLower(err.Error())
  150. return strings.Contains(s, "does not exist") && strings.Contains(s, "relation")
  151. }