Fix Docker boot hang when Spotify is the configured primary source.

Avoid blocking Spotify auth probes during gunicorn worker import and add a request timeout to the auth probe client so unreachable API calls cannot stall startup indefinitely.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Siddharth Pradhan 2026-06-29 12:08:03 -04:00
parent 36986e4668
commit 9b18e99419
6 changed files with 48 additions and 4 deletions

View file

@ -139,6 +139,7 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1
ENV DATABASE_PATH=/app/data/music_library.db
ENV SOULSYNC_CONFIG_PATH=/app/config/config.json
ENV PUID=1000
ENV PGID=1000
ENV UMASK=022

View file

@ -352,10 +352,21 @@ def get_hydrabase_client(allow_fallback: bool = True, require_enabled: bool = Tr
return None
def get_configured_primary_source() -> str:
"""Return metadata.fallback_source as stored in config, without runtime downgrade.
Unlike get_primary_source(), this never probes Spotify auth. Use at boot and
anywhere blocking network I/O must be avoided (gunicorn worker import, Docker
cold start when Spotify is unreachable).
"""
_default = METADATA_SOURCE_PRIORITY[0]
return _get_config_value("metadata.fallback_source", _default) or _default
def get_primary_source(spotify_client_factory: Optional[MetadataClientFactory] = None) -> str:
"""Return configured primary metadata source."""
_default = METADATA_SOURCE_PRIORITY[0]
source = _get_config_value("metadata.fallback_source", _default) or _default
source = get_configured_primary_source()
if source == "spotify":
try:

View file

@ -49,6 +49,7 @@ from core.metadata.registry import (
get_discogs_client,
get_hydrabase_client,
get_itunes_client,
get_configured_primary_source,
get_primary_client,
get_primary_source,
get_primary_source_label,
@ -117,6 +118,7 @@ __all__ = [
"get_metadata_service",
"get_musicmap_similar_artists",
"get_primary_client",
"get_configured_primary_source",
"get_primary_source",
"get_primary_source_label",
"get_spotify_client_for_profile",

View file

@ -857,7 +857,8 @@ class SpotifyClient:
# Use a dedicated probe client (retries=0) so a 429 here propagates
# immediately and we can detect long Retry-After bans.
try:
probe = spotipy.Spotify(auth_manager=self.sp.auth_manager, retries=0)
probe = spotipy.Spotify(
auth_manager=self.sp.auth_manager, retries=0, requests_timeout=15)
probe.current_user()
result = True
except Exception as e:

View file

@ -0,0 +1,27 @@
"""Tests for boot-safe configured primary source lookup."""
from unittest.mock import MagicMock, patch
from core.metadata import registry
def test_get_configured_primary_source_reads_config_without_auth_probe(monkeypatch):
monkeypatch.setattr(
registry,
"_get_config_value",
lambda key, default=None: "spotify" if key == "metadata.fallback_source" else default,
)
with patch.object(registry, "get_spotify_client") as get_client:
assert registry.get_configured_primary_source() == "spotify"
get_client.assert_not_called()
def test_get_primary_source_still_downgrades_unauthenticated_spotify(monkeypatch):
monkeypatch.setattr(registry, "get_configured_primary_source", lambda: "spotify")
spotify = MagicMock()
spotify.is_spotify_authenticated.return_value = False
monkeypatch.setattr(registry, "get_spotify_client", lambda **_: spotify)
assert registry.get_primary_source() == registry.METADATA_SOURCE_PRIORITY[0]

View file

@ -36242,11 +36242,13 @@ except Exception as e:
# they explicitly want background Spotify enrichment.
spotify_enrichment_worker = None
try:
from core.metadata_service import get_primary_source as _get_primary_source
from core.metadata_service import get_configured_primary_source
from database.music_database import MusicDatabase
spotify_enrichment_db = MusicDatabase()
spotify_enrichment_worker = SpotifyWorker(database=spotify_enrichment_db)
_primary = _get_primary_source()
# Use configured source only — get_primary_source() probes Spotify auth and can
# block gunicorn worker boot indefinitely when the API is unreachable.
_primary = get_configured_primary_source()
_user_paused = config_manager.get('spotify_enrichment_paused', False)
if _user_paused or _primary != 'spotify':
spotify_enrichment_worker.paused = True # Set BEFORE start() to prevent race condition