Files
silo-server/internal/taskmanager/tasks/settings_tasks.go
T
QuickandClaude Fable 5 4fe417ce27 feat(settings): sweep expired mutation receipts daily
Setting-mutation idempotency receipts were written with an expires_at that
nothing enforced, so the table grew forever. Add a hidden daily system task
(05:00) that walks every login account, opens its user store, and calls
DeleteExpiredSettingMutations. A user whose store fails to open or sweep is
logged and skipped so one broken store cannot stall retention for everyone
else; the delete is idempotent, so the next run repairs anything missed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 03:03:23 +00:00

65 lines
2.5 KiB
Go

package tasks
import (
"context"
"encoding/json"
"fmt"
"github.com/Silo-Server/silo-server/internal/taskmanager"
"github.com/Silo-Server/silo-server/internal/userstore"
)
// SettingMutationSweeper is the per-user retention sweep the task drives.
// Satisfied by *userstore.SettingMutationSweeper.
type SettingMutationSweeper interface {
Sweep(ctx context.Context, progress func(percent int, message string)) (userstore.SettingMutationSweepStats, error)
}
// SettingMutationsRetentionTask deletes expired setting-mutation idempotency
// receipts across every user store. The mutation endpoint guarantees a
// mutation_id is replayable for at least 30 days; expires_at is not
// self-enforcing, so this daily sweep is what actually bounds the table.
type SettingMutationsRetentionTask struct {
sweeper SettingMutationSweeper
}
// NewSettingMutationsRetentionTask creates the retention task.
func NewSettingMutationsRetentionTask(sweeper SettingMutationSweeper) *SettingMutationsRetentionTask {
return &SettingMutationsRetentionTask{sweeper: sweeper}
}
func (t *SettingMutationsRetentionTask) Key() string { return "setting_mutations_retention" }
func (t *SettingMutationsRetentionTask) Name() string { return "Clean Up Setting Mutation Receipts" }
func (t *SettingMutationsRetentionTask) Description() string {
return "Deletes expired settings-mutation idempotency receipts from every user's store."
}
func (t *SettingMutationsRetentionTask) Category() taskmanager.TaskCategory {
return taskmanager.TaskCategorySystem
}
func (t *SettingMutationsRetentionTask) IsHidden() bool { return true }
func (t *SettingMutationsRetentionTask) DefaultTriggers() []taskmanager.TriggerConfig {
return []taskmanager.TriggerConfig{{Type: taskmanager.TriggerTypeDaily, TimeOfDay: "05:00"}}
}
func (t *SettingMutationsRetentionTask) Execute(ctx context.Context, progress taskmanager.ProgressReporter) error {
if t == nil || t.sweeper == nil {
progress.Report(100, "Setting mutation sweep is not configured")
return nil
}
progress.Report(0, "Sweeping expired setting mutation receipts")
stats, err := t.sweeper.Sweep(ctx, func(percent int, message string) {
progress.Report(float64(percent), message)
})
if err != nil {
return fmt.Errorf("setting mutation retention: %w", err)
}
if data, err := json.Marshal(stats); err == nil {
progress.SetResultData(data)
}
progress.Report(100, fmt.Sprintf(
"Removed %d expired receipts across %d users (%d users failed)",
stats.ReceiptsDeleted, stats.UsersSwept, stats.UsersFailed))
return nil
}