Files

100 lines
3.4 KiB
Go
Raw Permalink Normal View History

package userstore
import (
"context"
"fmt"
"log/slog"
"sort"
"time"
"github.com/Silo-Server/silo-server/internal/models"
)
// UserLister enumerates login accounts for cross-user maintenance sweeps.
// Satisfied by *auth.UserRepository.
type UserLister interface {
List(ctx context.Context) ([]*models.User, error)
}
// SettingMutationSweepStats summarizes one retention sweep.
type SettingMutationSweepStats struct {
// UsersSwept counts users whose store was swept successfully.
UsersSwept int `json:"users_swept"`
// UsersFailed counts users skipped because their store could not be opened
// or swept. The sweep continues past them; the next run picks them up.
UsersFailed int `json:"users_failed"`
// ReceiptsDeleted is the total number of expired receipts removed.
ReceiptsDeleted int64 `json:"receipts_deleted"`
}
// SettingMutationSweeper deletes expired setting-mutation idempotency receipts
// from every user's store. A receipt's expires_at is not self-enforcing —
// PutSettingMutation only records it — so without this sweep receipts
// accumulate forever.
type SettingMutationSweeper struct {
users UserLister
stores UserStoreProvider
logger *slog.Logger
}
// NewSettingMutationSweeper wires the sweeper.
func NewSettingMutationSweeper(users UserLister, stores UserStoreProvider) *SettingMutationSweeper {
return &SettingMutationSweeper{
users: users,
stores: stores,
logger: slog.Default().With("component", "userstore.setting_mutation_sweep"),
}
}
// Sweep removes every receipt that has expired as of now, one user store at a
// time. A user whose store fails to open or sweep is logged and skipped so one
// broken store cannot stall retention for everyone else; nothing is
// checkpointed because the delete is idempotent and the next run repairs any
// user this one missed.
func (s *SettingMutationSweeper) Sweep(ctx context.Context, progress func(percent int, message string)) (SettingMutationSweepStats, error) {
report := func(percent int, message string) {
if progress != nil {
progress(percent, message)
}
}
var stats SettingMutationSweepStats
if s == nil || s.users == nil || s.stores == nil {
return stats, fmt.Errorf("setting mutation sweep requires a user lister and a store provider")
}
users, err := s.users.List(ctx)
if err != nil {
return stats, fmt.Errorf("list users: %w", err)
}
sort.Slice(users, func(i, j int) bool { return users[i].ID < users[j].ID })
cutoff := time.Now().UTC()
for idx, user := range users {
if ctx.Err() != nil {
return stats, ctx.Err()
}
store, err := s.stores.ForUser(ctx, user.ID)
if err != nil {
s.logger.WarnContext(ctx, "setting mutation sweep: open user store failed", "user_id", user.ID, "error", err)
stats.UsersFailed++
continue
}
deleted, err := store.DeleteExpiredSettingMutations(ctx, cutoff)
if err != nil {
if ctx.Err() != nil {
return stats, ctx.Err()
}
s.logger.WarnContext(ctx, "setting mutation sweep: user sweep failed", "user_id", user.ID, "error", err)
stats.UsersFailed++
continue
}
stats.UsersSwept++
stats.ReceiptsDeleted += deleted
report((idx+1)*100/max(len(users), 1),
fmt.Sprintf("Swept %d expired receipts across %d users", stats.ReceiptsDeleted, stats.UsersSwept))
}
s.logger.InfoContext(ctx, "setting mutation sweep completed",
"users", stats.UsersSwept, "failed", stats.UsersFailed, "receipts_deleted", stats.ReceiptsDeleted)
return stats, nil
}