Files

92 lines
2.9 KiB
Go
Raw Permalink Normal View History

package userstore
import (
"context"
"fmt"
"log/slog"
"sort"
)
// SettingValuesCleaner removes canonical library- and series-scoped setting
// values when their owning entity is deleted. The schema deliberately declares
// no foreign keys on library_id or series_id — libraries and series live in
// the shared catalog while values live per user — so the owning delete paths
// must call this, or removed content leaves orphaned preferences behind and a
// returning series identity resurrects stale values.
type SettingValuesCleaner struct {
users UserLister
stores UserStoreProvider
logger *slog.Logger
}
// NewSettingValuesCleaner wires the cleaner.
func NewSettingValuesCleaner(users UserLister, stores UserStoreProvider) *SettingValuesCleaner {
return &SettingValuesCleaner{
users: users,
stores: stores,
logger: slog.Default().With("component", "userstore.setting_values_cleanup"),
}
}
// DeleteForLibrary removes every user's profile_library values for one
// library. A user whose store fails is logged and skipped rather than failing
// the caller's delete: the library is already gone, and an orphaned row's only
// cost is dead weight until a retry — while aborting the delete job for it
// would leave far more state behind.
func (c *SettingValuesCleaner) DeleteForLibrary(ctx context.Context, libraryID int) int64 {
return c.sweep(ctx, fmt.Sprintf("library %d", libraryID),
func(ctx context.Context, store UserStore) (int64, error) {
return store.DeleteSettingValuesForLibrary(ctx, libraryID)
})
}
// DeleteForSeries removes every user's profile_series values for one series.
func (c *SettingValuesCleaner) DeleteForSeries(ctx context.Context, seriesID string) int64 {
return c.sweep(ctx, "series "+seriesID,
func(ctx context.Context, store UserStore) (int64, error) {
return store.DeleteSettingValuesForSeries(ctx, seriesID)
})
}
func (c *SettingValuesCleaner) sweep(
ctx context.Context,
subject string,
del func(context.Context, UserStore) (int64, error),
) int64 {
if c == nil || c.users == nil || c.stores == nil {
return 0
}
users, err := c.users.List(ctx)
if err != nil {
c.logger.WarnContext(ctx, "settings cleanup: list users failed",
"subject", subject, "error", err)
return 0
}
sort.Slice(users, func(i, j int) bool { return users[i].ID < users[j].ID })
var deleted int64
for _, user := range users {
if ctx.Err() != nil {
return deleted
}
store, err := c.stores.ForUser(ctx, user.ID)
if err != nil {
c.logger.WarnContext(ctx, "settings cleanup: open user store failed",
"subject", subject, "user_id", user.ID, "error", err)
continue
}
n, err := del(ctx, store)
if err != nil {
c.logger.WarnContext(ctx, "settings cleanup: delete failed",
"subject", subject, "user_id", user.ID, "error", err)
continue
}
deleted += n
}
if deleted > 0 {
c.logger.InfoContext(ctx, "settings cleanup removed orphaned values",
"subject", subject, "rows", deleted)
}
return deleted
}