From 5ba8a9fcb597799212eb48c6c9bbb429fe25b29d Mon Sep 17 00:00:00 2001 From: Deluan Date: Fri, 14 Aug 2026 13:15:24 -0400 Subject: [PATCH] feat(artwork): read a single queue row by item The explain CLI must report whether an item is queued, at what priority and when it retries; the queue repository could only be drained in eligibility batches, which cannot see a row that is still backing off. --- model/artwork.go | 2 ++ persistence/artwork_queue_repository.go | 10 ++++++++++ persistence/artwork_queue_repository_test.go | 16 ++++++++++++++++ tests/mock_artwork_queue_repo.go | 13 +++++++++++++ 4 files changed, 41 insertions(+) diff --git a/model/artwork.go b/model/artwork.go index 2612f6846..67a5cf45d 100644 --- a/model/artwork.go +++ b/model/artwork.go @@ -119,6 +119,8 @@ type ArtworkRepository interface { } type ArtworkQueueRepository interface { + // Get returns the pending row for an item, or ErrNotFound when it is not queued. + Get(kind Kind, id, imageType string) (*ArtworkQueueItem, error) // Enqueue upserts; an existing row keeps the higher priority and has its retry_at reset. Enqueue(items ...ArtworkQueueItem) error // EnqueuePreservingBackoff upserts like Enqueue but preserves an existing row's retry_at, so a diff --git a/persistence/artwork_queue_repository.go b/persistence/artwork_queue_repository.go index 526d9c9d1..22385d448 100644 --- a/persistence/artwork_queue_repository.go +++ b/persistence/artwork_queue_repository.go @@ -31,6 +31,16 @@ func NewArtworkQueueRepository(ctx context.Context, db dbx.Builder) model.Artwor return r } +func (r *artworkQueueRepository) Get(kind model.Kind, id, imageType string) (*model.ArtworkQueueItem, error) { + var res model.ArtworkQueueItem + err := r.queryOne(Select("*").From(r.tableName). + Where(Eq{"item_kind": kind.Prefix(), "item_id": id, "image_type": imageType}), &res) + if err != nil { + return nil, err + } + return &res, nil +} + // Enqueue also resets enqueued_at, so a fresh request does not inherit an old row's spent retry budget. func (r *artworkQueueRepository) Enqueue(items ...model.ArtworkQueueItem) error { return r.enqueue(`ON CONFLICT (item_kind, item_id, image_type) DO UPDATE SET diff --git a/persistence/artwork_queue_repository_test.go b/persistence/artwork_queue_repository_test.go index 816b314a4..8ac5407bb 100644 --- a/persistence/artwork_queue_repository_test.go +++ b/persistence/artwork_queue_repository_test.go @@ -54,6 +54,22 @@ var _ = Describe("ArtworkQueueRepository", func() { Expect(got[0].ItemID).To(Equal("high")) }) + It("Get returns a queued row, including one still backing off", func() { + Expect(repo.Enqueue(item("ar", "g1", model.ArtworkPriorityScan))).To(Succeed()) + backOff("ar", "g1", time.Now().Add(time.Hour)) + + got, err := repo.Get(model.KindArtistArtwork, "g1", model.ImageTypePrimary) + Expect(err).ToNot(HaveOccurred()) + Expect(got.Priority).To(Equal(model.ArtworkPriorityScan)) + Expect(got.Attempts).To(Equal(1)) + Expect(got.RetryAt).To(BeTemporally(">", time.Now())) + }) + + It("Get reports ErrNotFound when the item is not queued", func() { + _, err := repo.Get(model.KindArtistArtwork, "nope", model.ImageTypePrimary) + Expect(err).To(MatchError(model.ErrNotFound)) + }) + It("keeps the higher priority on duplicate enqueue", func() { Expect(repo.Enqueue(item("al", "a1", model.ArtworkPriorityBump))).To(Succeed()) Expect(repo.Enqueue(item("al", "a1", model.ArtworkPriorityBackfill))).To(Succeed()) diff --git a/tests/mock_artwork_queue_repo.go b/tests/mock_artwork_queue_repo.go index 0eeef465a..458db6b9f 100644 --- a/tests/mock_artwork_queue_repo.go +++ b/tests/mock_artwork_queue_repo.go @@ -25,6 +25,19 @@ func CreateMockArtworkQueueRepo() *MockArtworkQueueRepo { return &MockArtworkQueueRepo{Data: map[string]model.ArtworkQueueItem{}} } +func (m *MockArtworkQueueRepo) Get(kind model.Kind, id, imageType string) (*model.ArtworkQueueItem, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.Err != nil { + return nil, m.Err + } + it, ok := m.Data[iaKey(kind.Prefix(), id, imageType)] + if !ok { + return nil, model.ErrNotFound + } + return &it, nil +} + func (m *MockArtworkQueueRepo) Enqueue(items ...model.ArtworkQueueItem) error { m.mu.Lock() defer m.mu.Unlock()