mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
The provisional read-through and dangling re-enqueue used Enqueue, whose upsert resets retry_at, so any browse of an unresolved entity that was backing off after an external failure made it immediately eligible again — defeating the exponential backoff during a provider outage. Add EnqueueBump, which raises priority but leaves an existing row's retry_at intact, and route the serving path through it. Scan and manual re-resolve keep Enqueue's reset (a detected change wants immediate retry).
217 lines
5.0 KiB
Go
217 lines
5.0 KiB
Go
package tests
|
|
|
|
import (
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/model"
|
|
)
|
|
|
|
type MockArtworkQueueRepo struct {
|
|
model.ArtworkQueueRepository
|
|
// mu guards Data so the worker's concurrent drain can hit this mock race-free.
|
|
mu sync.Mutex
|
|
Data map[string]model.ArtworkQueueItem // keyed by iaKey(kind, id, imageType)
|
|
Err error
|
|
// ItemArtworkSource, when set, backs EnqueueStaleAbsent with real item_artwork state.
|
|
ItemArtworkSource *MockArtworkRepo
|
|
// ExistingIDs, keyed by item_kind, backs PurgeDangling; a nil per-kind map keeps that kind.
|
|
ExistingIDs map[string]map[string]bool
|
|
}
|
|
|
|
func CreateMockArtworkQueueRepo() *MockArtworkQueueRepo {
|
|
return &MockArtworkQueueRepo{Data: map[string]model.ArtworkQueueItem{}}
|
|
}
|
|
|
|
func (m *MockArtworkQueueRepo) Enqueue(items ...model.ArtworkQueueItem) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.Err != nil {
|
|
return m.Err
|
|
}
|
|
now := time.Now()
|
|
for _, it := range items {
|
|
if it.ImageType == "" {
|
|
it.ImageType = model.ImageTypePrimary
|
|
}
|
|
k := iaKey(it.ItemKind, it.ItemID, it.ImageType)
|
|
// Mirror the SQL: retry_at/enqueued_at are server-set, never taken from the caller.
|
|
if prev, ok := m.Data[k]; ok {
|
|
prev.Priority = max(prev.Priority, it.Priority)
|
|
prev.RetryAt = now
|
|
m.Data[k] = prev
|
|
continue
|
|
}
|
|
it.Attempts = 0
|
|
it.RetryAt = now
|
|
it.EnqueuedAt = now
|
|
m.Data[k] = it
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockArtworkQueueRepo) DequeueBatch(n int) ([]model.ArtworkQueueItem, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.Err != nil {
|
|
return nil, m.Err
|
|
}
|
|
var res []model.ArtworkQueueItem
|
|
now := time.Now()
|
|
for _, it := range m.Data {
|
|
if !it.RetryAt.After(now) {
|
|
res = append(res, it)
|
|
}
|
|
}
|
|
sort.Slice(res, func(i, j int) bool {
|
|
if res[i].Priority != res[j].Priority {
|
|
return res[i].Priority > res[j].Priority
|
|
}
|
|
return res[i].EnqueuedAt.Before(res[j].EnqueuedAt)
|
|
})
|
|
if len(res) > n {
|
|
res = res[:n]
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (m *MockArtworkQueueRepo) MarkFailed(kind, id, imageType string, retryAt time.Time) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.Err != nil {
|
|
return m.Err
|
|
}
|
|
k := iaKey(kind, id, imageType)
|
|
it, ok := m.Data[k]
|
|
if !ok {
|
|
return model.ErrNotFound
|
|
}
|
|
it.Attempts++
|
|
it.RetryAt = retryAt
|
|
m.Data[k] = it
|
|
return nil
|
|
}
|
|
|
|
func (m *MockArtworkQueueRepo) MarkFailedIfUnchanged(kind, id, imageType string, seenRetryAt, retryAt time.Time) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.Err != nil {
|
|
return m.Err
|
|
}
|
|
k := iaKey(kind, id, imageType)
|
|
if it, ok := m.Data[k]; ok && it.RetryAt.Equal(seenRetryAt) {
|
|
it.Attempts++
|
|
it.RetryAt = retryAt
|
|
m.Data[k] = it
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockArtworkQueueRepo) Delete(kind, id, imageType string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.Err != nil {
|
|
return m.Err
|
|
}
|
|
delete(m.Data, iaKey(kind, id, imageType))
|
|
return nil
|
|
}
|
|
|
|
func (m *MockArtworkQueueRepo) DeleteIfUnchanged(kind, id, imageType string, retryAt time.Time) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.Err != nil {
|
|
return m.Err
|
|
}
|
|
k := iaKey(kind, id, imageType)
|
|
if it, ok := m.Data[k]; ok && it.RetryAt.Equal(retryAt) {
|
|
delete(m.Data, k)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockArtworkQueueRepo) PurgeDangling() (int64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.Err != nil {
|
|
return 0, m.Err
|
|
}
|
|
var purged int64
|
|
for k, it := range m.Data {
|
|
existing := m.ExistingIDs[it.ItemKind]
|
|
if existing == nil {
|
|
continue
|
|
}
|
|
if !existing[it.ItemID] {
|
|
delete(m.Data, k)
|
|
purged++
|
|
}
|
|
}
|
|
return purged, nil
|
|
}
|
|
|
|
func (m *MockArtworkQueueRepo) Count() (int64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.Err != nil {
|
|
return 0, m.Err
|
|
}
|
|
return int64(len(m.Data)), nil
|
|
}
|
|
|
|
func (m *MockArtworkQueueRepo) EnqueueBump(items ...model.ArtworkQueueItem) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.Err != nil {
|
|
return m.Err
|
|
}
|
|
now := time.Now()
|
|
for _, it := range items {
|
|
if it.ImageType == "" {
|
|
it.ImageType = model.ImageTypePrimary
|
|
}
|
|
k := iaKey(it.ItemKind, it.ItemID, it.ImageType)
|
|
// Preserve an existing row's retry_at: a bump raises priority without resetting backoff.
|
|
if prev, ok := m.Data[k]; ok {
|
|
prev.Priority = max(prev.Priority, it.Priority)
|
|
m.Data[k] = prev
|
|
continue
|
|
}
|
|
it.Attempts = 0
|
|
it.RetryAt = now
|
|
it.EnqueuedAt = now
|
|
m.Data[k] = it
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockArtworkQueueRepo) EnqueueStaleAbsent(kind string, attemptedBefore time.Time) (int64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.Err != nil || m.ItemArtworkSource == nil {
|
|
return 0, m.Err
|
|
}
|
|
now := time.Now()
|
|
var inserted int64
|
|
for _, ia := range m.ItemArtworkSource.ItemData {
|
|
if ia.ItemKind != kind || ia.Hash != "" || !ia.AttemptedAt.Before(attemptedBefore) {
|
|
continue
|
|
}
|
|
k := iaKey(ia.ItemKind, ia.ItemID, ia.ImageType)
|
|
if _, ok := m.Data[k]; ok { // DO NOTHING: never touch existing queue rows
|
|
continue
|
|
}
|
|
m.Data[k] = model.ArtworkQueueItem{
|
|
ItemKind: ia.ItemKind,
|
|
ItemID: ia.ItemID,
|
|
ImageType: ia.ImageType,
|
|
Priority: model.ArtworkPriorityRecheck,
|
|
RetryAt: now,
|
|
EnqueuedAt: now,
|
|
}
|
|
inserted++
|
|
}
|
|
return inserted, nil
|
|
}
|