retention: write the audit row before the actions_log cap

RetainWithAudit appends the 'retain' audit row inside the same transaction and
before the cap, so the cap counts it. After a run, actions_log holds at most
EMAIL_AGENT_ACTIONS_LOG_MAX_ROWS rows, including the audit row. Adds a test.
This commit is contained in:
Builder
2026-10-13 10:42:44 +11:00
parent e5cce1c93a
commit d8788ef8e7
3 changed files with 50 additions and 7 deletions

View File

@@ -344,16 +344,14 @@ func runRetain(cfg *config.Config, logger *slog.Logger) error {
BodyMaxAge: time.Duration(cfg.RetentionDays) * 24 * time.Hour,
ActionsLogMaxRows: cfg.ActionsLogMaxRows,
}
res, err := st.Retain(ctx, policy)
// The audit row is written inside the same transaction, before the cap, so
// the cap counts it. This is what keeps actions_log bounded across runs.
detail := fmt.Sprintf("bodies_cleared=%%d retention_days=%d max_rows=%d",
cfg.RetentionDays, cfg.ActionsLogMaxRows)
res, err := st.RetainWithAudit(ctx, policy, "scheduler", detail)
if err != nil {
return err
}
if err := st.LogAction(ctx, "retain", "scheduler",
fmt.Sprintf("bodies_cleared=%d actions_deleted=%d retention_days=%d max_rows=%d",
res.BodiesCleared, res.ActionsDeleted, cfg.RetentionDays, cfg.ActionsLogMaxRows)); err != nil {
// Retention already committed; a failed audit row must not fail the job.
logger.Error("log retention action", "error", err)
}
cutoff := "disabled"
if !res.BodyCutoff.IsZero() {