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:
@@ -49,6 +49,14 @@ type RetentionResult struct {
|
||||
// pass never leaves the database in a mixed state. It is safe to run on a
|
||||
// schedule: every statement is idempotent.
|
||||
func (s *Store) Retain(ctx context.Context, p RetentionPolicy) (RetentionResult, error) {
|
||||
return s.RetainWithAudit(ctx, p, "", "")
|
||||
}
|
||||
|
||||
// RetainWithAudit is Retain plus one actions_log row (FR-13), appended inside
|
||||
// the same transaction before the cap is applied so the audit row is itself
|
||||
// counted by the cap. An empty detail writes no audit row. This keeps the
|
||||
// action log bounded even though every run records that it ran.
|
||||
func (s *Store) RetainWithAudit(ctx context.Context, p RetentionPolicy, actor, detail string) (RetentionResult, error) {
|
||||
var res RetentionResult
|
||||
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
@@ -74,6 +82,14 @@ WHERE body_text IS NOT NULL AND body_text <> '' AND received_at < ?`,
|
||||
}
|
||||
}
|
||||
|
||||
if detail != "" {
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO actions_log (message_id, action, actor, detail)
|
||||
VALUES (NULL, 'retain', ?, ?)`, nullString(actor), detail); err != nil {
|
||||
return res, fmt.Errorf("log retention action: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if p.ActionsLogMaxRows > 0 {
|
||||
// Keep the newest N rows and delete the rest. id is monotonic, so it
|
||||
// is the stable ordering for an append-only table.
|
||||
|
||||
Reference in New Issue
Block a user