New creates a new periodically purging database instance. Callers must Close the returned instance. The auditor pointer is loaded on each dispatch tick so runtime entitlement changes (e.g. toggling the audit-log feature) take effect without restarting the process. Notifications enqueuer defaults to
(ctx context.Context, logger slog.Logger, db database.Store, vals *codersdk.DeploymentValues, reg prometheus.Registerer, auditor *atomic.Pointer[audit.Auditor], opts ...Option)
| 100 | // defaults to no-op. Use WithNotificationsEnqueuer to pass a real |
| 101 | // one. |
| 102 | func New(ctx context.Context, logger slog.Logger, db database.Store, vals *codersdk.DeploymentValues, reg prometheus.Registerer, auditor *atomic.Pointer[audit.Auditor], opts ...Option) io.Closer { |
| 103 | closed := make(chan struct{}) |
| 104 | |
| 105 | ctx, cancelFunc := context.WithCancel(ctx) |
| 106 | //nolint:gocritic // Use dbpurge-specific subject with minimal permissions. |
| 107 | ctx = dbauthz.AsDBPurge(ctx) |
| 108 | |
| 109 | iterationDuration := prometheus.NewHistogramVec(prometheus.HistogramOpts{ |
| 110 | Namespace: "coderd", |
| 111 | Subsystem: "dbpurge", |
| 112 | Name: "iteration_duration_seconds", |
| 113 | Help: "Duration of each dbpurge iteration in seconds.", |
| 114 | Buckets: []float64{1, 5, 10, 30, 60, 300, 600}, // 1s to 10min |
| 115 | }, []string{"success"}) |
| 116 | reg.MustRegister(iterationDuration) |
| 117 | |
| 118 | recordsPurged := prometheus.NewCounterVec(prometheus.CounterOpts{ |
| 119 | Namespace: "coderd", |
| 120 | Subsystem: "dbpurge", |
| 121 | Name: "records_purged_total", |
| 122 | Help: "Total number of records purged by type.", |
| 123 | }, []string{"record_type"}) |
| 124 | reg.MustRegister(recordsPurged) |
| 125 | |
| 126 | chatAutoArchiveRecords := prometheus.NewCounter(prometheus.CounterOpts{ |
| 127 | Namespace: "coderd", |
| 128 | Subsystem: "chat_auto_archive", |
| 129 | Name: "records_archived_total", |
| 130 | Help: "Total number of chats archived by the auto-archive job (counting both roots and cascaded children).", |
| 131 | }) |
| 132 | reg.MustRegister(chatAutoArchiveRecords) |
| 133 | |
| 134 | inst := &instance{ |
| 135 | cancel: cancelFunc, |
| 136 | closed: closed, |
| 137 | logger: logger, |
| 138 | vals: vals, |
| 139 | clk: quartz.NewReal(), |
| 140 | auditor: auditor, |
| 141 | enqueuer: notifications.NewNoopEnqueuer(), |
| 142 | iterationDuration: iterationDuration, |
| 143 | recordsPurged: recordsPurged, |
| 144 | chatAutoArchiveRecords: chatAutoArchiveRecords, |
| 145 | chatAutoArchiveBatchSize: defaultChatAutoArchiveBatchSize, |
| 146 | } |
| 147 | for _, opt := range opts { |
| 148 | opt(inst) |
| 149 | } |
| 150 | |
| 151 | // Start the ticker with the initial delay. |
| 152 | ticker := inst.clk.NewTicker(delay) |
| 153 | doTick := func(ctx context.Context, start time.Time) { |
| 154 | defer ticker.Reset(delay) |
| 155 | err := inst.purgeTick(ctx, db, start) |
| 156 | if err != nil { |
| 157 | logger.Error(ctx, "failed to purge old database entries", slog.Error(err)) |
| 158 | |
| 159 | // Record metrics for failed purge iteration. |