mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-18 07:30:29 +00:00
feat(radio): back off long on permanent ICY errors
Dead stream URLs (4xx) and stations without icy-metaint retried every 30s for the whole session; classify them permanent and retry every 10 minutes instead. Signed-off-by: hiyukoim <11803292+hiyukoim@users.noreply.github.com>
This commit is contained in:
parent
989d15074a
commit
fd3c3b2be2
@ -19,6 +19,29 @@ var (
|
||||
// keep an ICY reader connected to the station forever.
|
||||
const defaultSessionTTL = 10 * time.Minute
|
||||
|
||||
// permanentRetryDelay is used instead of the regular backoff when the reader
|
||||
// reports a permanent error (dead URL, station without ICY metadata), so we
|
||||
// do not hammer a stream that will never yield titles.
|
||||
const permanentRetryDelay = 10 * time.Minute
|
||||
|
||||
type permanentError struct{ err error }
|
||||
|
||||
func (e *permanentError) Error() string { return e.err.Error() }
|
||||
func (e *permanentError) Unwrap() error { return e.err }
|
||||
|
||||
// MarkPermanent wraps err so the retry loop switches to permanentRetryDelay.
|
||||
func MarkPermanent(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return &permanentError{err: err}
|
||||
}
|
||||
|
||||
func isPermanent(err error) bool {
|
||||
var p *permanentError
|
||||
return errors.As(err, &p)
|
||||
}
|
||||
|
||||
type Station struct {
|
||||
ID string
|
||||
StreamURL string
|
||||
@ -229,6 +252,9 @@ func (m *MetadataManager) runReader(reader *activeReader) {
|
||||
}
|
||||
|
||||
delay := m.backoff(attempt)
|
||||
if isPermanent(err) && delay < permanentRetryDelay {
|
||||
delay = permanentRetryDelay
|
||||
}
|
||||
if delay <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -155,6 +155,25 @@ var _ = Describe("MetadataManager", func() {
|
||||
Expect(manager.Start(context.Background(), "session-1", Station{ID: "rd-1", StreamURL: ""})).To(MatchError(ErrInvalidStation))
|
||||
})
|
||||
|
||||
It("backs off for a long time on permanent reader errors", func() {
|
||||
reader.err = MarkPermanent(errors.New("missing icy metadata interval"))
|
||||
slow := NewMetadataManager(
|
||||
reader.Read,
|
||||
publisher.Publish,
|
||||
WithRetryBackoff(func(int) time.Duration { return time.Millisecond }),
|
||||
)
|
||||
|
||||
Expect(slow.Start(context.Background(), "session-1", Station{
|
||||
ID: "rd-1",
|
||||
StreamURL: "https://stream.example.test/radio",
|
||||
})).To(Succeed())
|
||||
|
||||
Eventually(reader.StartCount).Should(Equal(1))
|
||||
// Regular backoff is 1ms; the permanent error must stretch it, so no
|
||||
// second attempt happens within the observation window.
|
||||
Consistently(reader.StartCount, 300*time.Millisecond, 20*time.Millisecond).Should(Equal(1))
|
||||
})
|
||||
|
||||
It("expires sessions that are never refreshed", func() {
|
||||
expiring := NewMetadataManager(
|
||||
reader.Read,
|
||||
|
||||
@ -2,6 +2,7 @@ package radio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
@ -26,6 +27,22 @@ var icyHTTPClient = &http.Client{
|
||||
|
||||
func NewMetadataManagerService(publisher TitlePublisher) *MetadataManager {
|
||||
return NewMetadataManager(func(ctx context.Context, streamURL string, handleTitle func(string)) error {
|
||||
return icy.ReadHTTPStreamTitles(ctx, icyHTTPClient, streamURL, handleTitle)
|
||||
return classifyICYError(icy.ReadHTTPStreamTitles(ctx, icyHTTPClient, streamURL, handleTitle))
|
||||
}, publisher)
|
||||
}
|
||||
|
||||
// classifyICYError marks errors that a retry will not fix — a stream without
|
||||
// ICY metadata or a client-error HTTP status — so the reader backs off for
|
||||
// much longer instead of hammering the station.
|
||||
func classifyICYError(err error) error {
|
||||
if errors.Is(err, icy.ErrMissingMetaInt) {
|
||||
return MarkPermanent(err)
|
||||
}
|
||||
var statusErr *icy.ResponseStatusError
|
||||
if errors.As(err, &statusErr) &&
|
||||
statusErr.StatusCode >= 400 && statusErr.StatusCode < 500 &&
|
||||
statusErr.StatusCode != http.StatusRequestTimeout && statusErr.StatusCode != http.StatusTooManyRequests {
|
||||
return MarkPermanent(err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user