Просмотр исходного кода

fix(storage): delete orphaned icons

The icons table is deduplicated by hash and shared with feeds via the
feed_icons junction. When a feed is deleted, the ON DELETE CASCADE
removes its feed_icons row but leaves the icons row behind. The same
happens in StoreFeedIcon when a feed's icon is replaced. Over time
these orphaned bytea blobs accumulate and bloat the database.

Add Storage.CleanupOrphanIcons, which deletes icons rows that no
feed_icons row still references, and call it from runCleanupTasks.
jvoisin 1 неделя назад
Родитель
Сommit
018128e109
2 измененных файлов с 27 добавлено и 0 удалено
  1. 8 0
      internal/cli/cleanup_tasks.go
  2. 19 0
      internal/storage/icon.go

+ 8 - 0
internal/cli/cleanup_tasks.go

@@ -47,4 +47,12 @@ func runCleanupTasks(store *storage.Storage) {
 			metric.ArchiveEntriesDuration.WithLabelValues(model.EntryStatusUnread).Observe(time.Since(startTime).Seconds())
 		}
 	}
+
+	if nbIcons, err := store.CleanupOrphanIcons(); err != nil {
+		slog.Error("Unable to clean orphan icons", slog.Any("error", err))
+	} else {
+		slog.Info("Orphan icons cleanup completed",
+			slog.Int64("orphan_icons_removed", nbIcons),
+		)
+	}
 }

+ 19 - 0
internal/storage/icon.go

@@ -146,6 +146,25 @@ func (s *Storage) StoreFeedIcon(feedID int64, icon *model.Icon) error {
 	return nil
 }
 
+// CleanupOrphanIcons removes icons that are no longer associated with any
+// feed. Such rows accumulate when feeds are deleted (the cascade only removes
+// the feed_icons mapping, not the dedup-by-hash icons row) or when a feed's
+// icon is replaced by StoreFeedIcon.
+func (s *Storage) CleanupOrphanIcons() (int64, error) {
+	result, err := s.db.Exec(`
+		DELETE FROM icons
+		WHERE NOT EXISTS (
+			SELECT 1 FROM feed_icons WHERE feed_icons.icon_id = icons.id
+		)
+	`)
+	if err != nil {
+		return 0, fmt.Errorf(`store: unable to clean orphan icons: %v`, err)
+	}
+
+	n, _ := result.RowsAffected()
+	return n, nil
+}
+
 // Icons lists all icons currently associated with any feed owned by the given user.
 func (s *Storage) Icons(userID int64) (model.Icons, error) {
 	query := `