From 8a0ff264f19cc5f37cc7e2ebff3c774294928027 Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 24 Jul 2026 15:49:30 -0400 Subject: [PATCH] feat(artwork): re-queue an absent cover when its page is viewed serveEntity now schedules a Bump recheck for an entity whose art was recorded absent, so viewing a missing cover re-triggers resolution (e.g. after an external source that was down during the scan comes back), matching the request-time bump that already covers never-resolved entities. Throttled by attempted_at against requestRecheckAge (1h) so repeatedly opening a genuinely-absent page can't hammer external services. EnqueueBump preserves an existing failed-state backoff via MAX(priority,...) and inserts a fresh, immediately-eligible recheck for a settled-absent row (whose queue row was already deleted). --- core/artwork/serving.go | 12 +++++++++++- core/artwork/serving_test.go | 20 ++++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/core/artwork/serving.go b/core/artwork/serving.go index f4e67cd19..a29e5325f 100644 --- a/core/artwork/serving.go +++ b/core/artwork/serving.go @@ -89,8 +89,13 @@ func (s *service) Get(ctx context.Context, artID model.ArtworkID, size int, squa } } +// requestRecheckAge throttles view-triggered rechecks of an absent entity so repeatedly opening a +// genuinely-absent page can't hammer external services; below staleAbsentAge to catch younger absences. +const requestRecheckAge = time.Hour + // serveEntity serves an entity whose state the worker owns (album/artist/playlist/radio): -// found row serves its hash, absent row is unavailable, missing row reads through provisionally. +// found row serves its hash, absent row is unavailable (promoting a stale recheck on view), +// missing row reads through provisionally. func (s *service) serveEntity(ctx context.Context, artID model.ArtworkID, size int, square bool) (*Image, error) { ia, err := s.ds.Artwork(ctx).GetItemArtwork(artID.Kind.Prefix(), artID.ID, model.ImageTypePrimary) switch { @@ -99,6 +104,11 @@ func (s *service) serveEntity(ctx context.Context, artID model.ArtworkID, size i case err != nil: return nil, err case ia.Hash == "": + // EnqueueBump preserves an existing backoff row's retry_at; for a settled absent row + // (no queue row) it inserts a fresh, immediately-eligible recheck. + if time.Since(ia.AttemptedAt) > requestRecheckAge { + s.enqueue(ctx, artID, model.ArtworkPriorityBump) + } return nil, ErrUnavailable default: return s.serveHash(ctx, artID, ia, size, square) diff --git a/core/artwork/serving_test.go b/core/artwork/serving_test.go index 0f344e4a0..dc20a358a 100644 --- a/core/artwork/serving_test.go +++ b/core/artwork/serving_test.go @@ -183,13 +183,29 @@ var _ = Describe("Service", func() { Expect(queueRepo.Data[primaryKey("al", "al3b")].Priority).To(Equal(model.ArtworkPriorityScan)) }) - It("returns ErrUnavailable for an absent state without enqueuing", func() { - Expect(artRepo.PutItemArtwork(&model.ItemArtwork{ItemKind: "al", ItemID: "al4"})).To(Succeed()) + It("does not re-enqueue a recently-attempted absent state", func() { + Expect(artRepo.PutItemArtwork(&model.ItemArtwork{ + ItemKind: "al", ItemID: "al4", AttemptedAt: time.Now(), + })).To(Succeed()) _, err := svc.Get(ctx, model.MustParseArtworkID("al-al4"), 0, false) Expect(err).To(MatchError(ErrUnavailable)) Expect(queueRepo.Data).To(BeEmpty()) }) + + It("promotes a stale absent state at Bump priority on view", func() { + Expect(artRepo.PutItemArtwork(&model.ItemArtwork{ + ItemKind: "al", ItemID: "al4b", AttemptedAt: time.Now().Add(-2 * requestRecheckAge), + })).To(Succeed()) + + _, err := svc.Get(ctx, model.MustParseArtworkID("al-al4b"), 0, false) + Expect(err).To(MatchError(ErrUnavailable)) + Expect(queueRepo.Data[primaryKey("al", "al4b")].Priority).To(Equal(model.ArtworkPriorityBump)) + // The absent state row is left intact; only a recheck is scheduled. + ia, err := artRepo.GetItemArtwork("al", "al4b", model.ImageTypePrimary) + Expect(err).ToNot(HaveOccurred()) + Expect(ia.Hash).To(BeEmpty()) + }) }) Describe("provisional read-through", func() {