fix(requests): clear prior failed rows on re-request

{"subject":"fix(requests): clear prior failed rows on re-request","body":""}
This commit is contained in:
Silo Server Migration
2026-05-25 10:02:06 -04:00
parent b5d9243942
commit 7a9d7bcc94
4 changed files with 74 additions and 0 deletions
+17
View File
@@ -182,6 +182,23 @@ func (r *Repository) ListActiveByTMDB(ctx context.Context, mediaType MediaType,
return out, nil
}
func (r *Repository) DeleteFailedByTMDB(ctx context.Context, mediaType MediaType, tmdbID int) (int, error) {
if tmdbID <= 0 {
return 0, nil
}
tag, err := r.pool.Exec(ctx, `
DELETE FROM media_requests
WHERE media_type = $1
AND provider = 'tmdb'
AND tmdb_id = $2
AND outcome = 'failed'
`, mediaType, tmdbID)
if err != nil {
return 0, fmt.Errorf("delete failed requests by tmdb: %w", err)
}
return int(tag.RowsAffected()), nil
}
// quotaLockNamespace partitions advisory locks so request-quota locks do not
// collide with advisory locks held elsewhere in the database. The value is
// arbitrary; what matters is that it is stable.
+6
View File
@@ -278,6 +278,12 @@ func (s *Service) CreateRequest(ctx context.Context, viewer Viewer, input Create
return nil, ErrAlreadyRequested
}
// Re-requesting media that previously failed (e.g., transient integration
// error) should not leave stale failed rows behind in user/admin lists.
if _, err := s.store.DeleteFailedByTMDB(ctx, normalized.MediaType, normalized.TMDBID); err != nil {
return nil, err
}
policy, err := s.EffectivePolicy(ctx, viewer.UserID)
if err != nil {
return nil, err
+48
View File
@@ -349,6 +349,41 @@ func TestCreateRequestNoActiveDuplicateCreatesRequest(t *testing.T) {
}
}
func TestCreateRequestClearsPriorFailedRequest(t *testing.T) {
store := newFakeStore()
store.settings.RequestsEnabled = true
store.requests["req-prior-failed"] = &Request{
ID: "req-prior-failed",
MediaType: MediaTypeMovie,
TMDBID: 550,
Outcome: OutcomeFailed,
Status: StatusApproved,
LastError: "arr: decode response: json: cannot unmarshal object into Go value of type []radarr.movieResource",
}
store.requests["req-other-media-failed"] = &Request{
ID: "req-other-media-failed",
MediaType: MediaTypeMovie,
TMDBID: 999,
Outcome: OutcomeFailed,
}
service := newTestService(store)
_, err := service.CreateRequest(context.Background(), testViewer(1), CreateRequestInput{
MediaType: MediaTypeMovie,
TMDBID: 550,
Title: "Fight Club",
})
if err != nil {
t.Fatalf("CreateRequest returned error: %v", err)
}
if _, ok := store.requests["req-prior-failed"]; ok {
t.Fatal("prior failed request was not cleared")
}
if _, ok := store.requests["req-other-media-failed"]; !ok {
t.Fatal("failed request for different media should not be cleared")
}
}
func TestSearchMarksSeriesAvailableByHydratedTVDBID(t *testing.T) {
store := newFakeStore()
store.settings.RequestsEnabled = true
@@ -905,6 +940,19 @@ func (f *fakeStore) ListActiveByTMDB(_ context.Context, mediaType MediaType, ids
return out, nil
}
func (f *fakeStore) DeleteFailedByTMDB(_ context.Context, mediaType MediaType, tmdbID int) (int, error) {
f.mu.Lock()
defer f.mu.Unlock()
deleted := 0
for id, req := range f.requests {
if req.MediaType == mediaType && req.TMDBID == tmdbID && req.Outcome == OutcomeFailed {
delete(f.requests, id)
deleted++
}
}
return deleted, nil
}
func (f *fakeStore) CreateRequest(_ context.Context, input CreateRequestRecord) (*Request, error) {
f.mu.Lock()
defer f.mu.Unlock()
+3
View File
@@ -12,6 +12,9 @@ type Store interface {
UpsertUserLimit(ctx context.Context, limit UserLimit) (*UserLimit, error)
CountUserRequestsSince(ctx context.Context, userID int, since time.Time) (int, error)
ListActiveByTMDB(ctx context.Context, mediaType MediaType, tmdbIDs []int) (map[int]*Request, error)
// DeleteFailedByTMDB removes prior failed requests for a given media so a
// re-request does not leave behind stale rows in user/admin lists.
DeleteFailedByTMDB(ctx context.Context, mediaType MediaType, tmdbID int) (int, error)
CreateRequest(ctx context.Context, input CreateRequestRecord) (*Request, error)
GetRequest(ctx context.Context, id string) (*Request, error)
ListReconciliationCandidates(ctx context.Context, limit int) ([]*Request, error)