100 lines
3.4 KiB
Go
100 lines
3.4 KiB
Go
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
|
||
|
|
}
|