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:
parent
36986e4668
commit
9b18e99419
6 changed files with 48 additions and 4 deletions
|
|
@ -139,6 +139,7 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
||||||
ENV PYTHONPATH=/app
|
ENV PYTHONPATH=/app
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
ENV DATABASE_PATH=/app/data/music_library.db
|
ENV DATABASE_PATH=/app/data/music_library.db
|
||||||
|
ENV SOULSYNC_CONFIG_PATH=/app/config/config.json
|
||||||
ENV PUID=1000
|
ENV PUID=1000
|
||||||
ENV PGID=1000
|
ENV PGID=1000
|
||||||
ENV UMASK=022
|
ENV UMASK=022
|
||||||
|
|
|
||||||
|
|
@ -352,10 +352,21 @@ def get_hydrabase_client(allow_fallback: bool = True, require_enabled: bool = Tr
|
||||||
return None
|
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:
|
def get_primary_source(spotify_client_factory: Optional[MetadataClientFactory] = None) -> str:
|
||||||
"""Return configured primary metadata source."""
|
"""Return configured primary metadata source."""
|
||||||
_default = METADATA_SOURCE_PRIORITY[0]
|
_default = METADATA_SOURCE_PRIORITY[0]
|
||||||
source = _get_config_value("metadata.fallback_source", _default) or _default
|
source = get_configured_primary_source()
|
||||||
|
|
||||||
if source == "spotify":
|
if source == "spotify":
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ from core.metadata.registry import (
|
||||||
get_discogs_client,
|
get_discogs_client,
|
||||||
get_hydrabase_client,
|
get_hydrabase_client,
|
||||||
get_itunes_client,
|
get_itunes_client,
|
||||||
|
get_configured_primary_source,
|
||||||
get_primary_client,
|
get_primary_client,
|
||||||
get_primary_source,
|
get_primary_source,
|
||||||
get_primary_source_label,
|
get_primary_source_label,
|
||||||
|
|
@ -117,6 +118,7 @@ __all__ = [
|
||||||
"get_metadata_service",
|
"get_metadata_service",
|
||||||
"get_musicmap_similar_artists",
|
"get_musicmap_similar_artists",
|
||||||
"get_primary_client",
|
"get_primary_client",
|
||||||
|
"get_configured_primary_source",
|
||||||
"get_primary_source",
|
"get_primary_source",
|
||||||
"get_primary_source_label",
|
"get_primary_source_label",
|
||||||
"get_spotify_client_for_profile",
|
"get_spotify_client_for_profile",
|
||||||
|
|
|
||||||
|
|
@ -857,7 +857,8 @@ class SpotifyClient:
|
||||||
# Use a dedicated probe client (retries=0) so a 429 here propagates
|
# Use a dedicated probe client (retries=0) so a 429 here propagates
|
||||||
# immediately and we can detect long Retry-After bans.
|
# immediately and we can detect long Retry-After bans.
|
||||||
try:
|
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()
|
probe.current_user()
|
||||||
result = True
|
result = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
27
tests/metadata/test_configured_primary_source.py
Normal file
27
tests/metadata/test_configured_primary_source.py
Normal 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]
|
||||||
|
|
@ -36242,11 +36242,13 @@ except Exception as e:
|
||||||
# they explicitly want background Spotify enrichment.
|
# they explicitly want background Spotify enrichment.
|
||||||
spotify_enrichment_worker = None
|
spotify_enrichment_worker = None
|
||||||
try:
|
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
|
from database.music_database import MusicDatabase
|
||||||
spotify_enrichment_db = MusicDatabase()
|
spotify_enrichment_db = MusicDatabase()
|
||||||
spotify_enrichment_worker = SpotifyWorker(database=spotify_enrichment_db)
|
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)
|
_user_paused = config_manager.get('spotify_enrichment_paused', False)
|
||||||
if _user_paused or _primary != 'spotify':
|
if _user_paused or _primary != 'spotify':
|
||||||
spotify_enrichment_worker.paused = True # Set BEFORE start() to prevent race condition
|
spotify_enrichment_worker.paused = True # Set BEFORE start() to prevent race condition
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue