Skip to content

Commit c97c81a

Browse files
committed
feat(api): make job timeout configurable via YUBAL_JOB_TIMEOUT_SECONDS
Users with large playlists (400+ tracks) hit the hardcoded 30-minute timeout. This replaces the class constant with a setting configurable via environment variable (default: 1800s). Refs #73
1 parent 3c154f0 commit c97c81a

File tree

7 files changed

+31
-17
lines changed

7 files changed

+31
-17
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ YUBAL_DOWNLOAD_UGC=false # Download user-generated content to _Unofficial/
2727
# Filenames
2828
YUBAL_ASCII_FILENAMES=false # Transliterate unicode to ASCII in filenames (default: false)
2929

30+
# Jobs
31+
YUBAL_JOB_TIMEOUT_SECONDS=1800 # Job execution timeout in seconds (default: 1800)
32+
3033
# Scheduler
3134
YUBAL_SCHEDULER_ENABLED=true # Enable scheduled sync (default: true)
3235
YUBAL_SCHEDULER_CRON="0 0 * * *" # Cron schedule (default: daily at midnight)

README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,17 @@ docker compose up -d
112112

113113
## ⚙️ Configuration
114114

115-
| Variable | Description | Default (Docker) |
116-
| ------------------------- | ------------------------------------------------- | ---------------- |
117-
| `YUBAL_AUDIO_FORMAT` | `opus`, `mp3`, or `m4a` | `opus` |
118-
| `YUBAL_AUDIO_QUALITY` | Transcode quality (0=best, 10=worst) | `0` |
119-
| `YUBAL_SCHEDULER_ENABLED` | Enable automatic scheduled sync | `true` |
120-
| `YUBAL_SCHEDULER_CRON` | Cron schedule for auto-sync | `0 0 * * *` |
121-
| `YUBAL_FETCH_LYRICS` | Fetch lyrics from lrclib.net | `true` |
122-
| `YUBAL_DOWNLOAD_UGC` | Download user-generated content to `_Unofficial/` | `false` |
123-
| `YUBAL_REPLAYGAIN` | Apply ReplayGain tags to downloads | `true` |
124-
| `YUBAL_TZ` | Timezone (IANA format) | `UTC` |
115+
| Variable | Description | Default (Docker) |
116+
| --------------------------- | ------------------------------------------------- | ---------------- |
117+
| `YUBAL_AUDIO_FORMAT` | `opus`, `mp3`, or `m4a` | `opus` |
118+
| `YUBAL_AUDIO_QUALITY` | Transcode quality (0=best, 10=worst) | `0` |
119+
| `YUBAL_SCHEDULER_ENABLED` | Enable automatic scheduled sync | `true` |
120+
| `YUBAL_SCHEDULER_CRON` | Cron schedule for auto-sync | `0 0 * * *` |
121+
| `YUBAL_FETCH_LYRICS` | Fetch lyrics from lrclib.net | `true` |
122+
| `YUBAL_DOWNLOAD_UGC` | Download user-generated content to `_Unofficial/` | `false` |
123+
| `YUBAL_REPLAYGAIN` | Apply ReplayGain tags to downloads | `true` |
124+
| `YUBAL_JOB_TIMEOUT_SECONDS` | Job execution timeout in seconds | `1800` |
125+
| `YUBAL_TZ` | Timezone (IANA format) | `UTC` |
125126

126127
<details>
127128
<summary>All options</summary>

deploy/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ YUBAL_DOWNLOAD_UGC=false # Download user-generated content to _Unofficial/
1717
# Filenames
1818
YUBAL_ASCII_FILENAMES=false # Transliterate unicode to ASCII in filenames (default: false)
1919

20+
# Jobs
21+
YUBAL_JOB_TIMEOUT_SECONDS=1800 # Job execution timeout in seconds (default: 1800)
22+
2023
# Scheduler
2124
YUBAL_SCHEDULER_ENABLED=true # Enable scheduled sync (default: true)
2225
YUBAL_SCHEDULER_CRON="0 0 * * *" # Cron expression (default: daily at midnight)

packages/api/src/yubal_api/api/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def create_services(repository: SubscriptionRepository) -> Services:
162162
download_ugc=settings.download_ugc,
163163
subscription_service=subscription_service,
164164
cache_path=settings.cache_path,
165+
job_timeout=settings.job_timeout_seconds,
165166
)
166167

167168
# Create scheduler

packages/api/src/yubal_api/services/job_executor.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,6 @@ class JobExecutor:
4141
- Tasks are tracked in a set to prevent garbage collection
4242
"""
4343

44-
TIMEOUT_SECONDS: float = 30 * 60 # 30 minutes
45-
4644
def __init__(
4745
self,
4846
job_store: JobExecutionStore,
@@ -55,6 +53,7 @@ def __init__(
5553
download_ugc: bool = False,
5654
subscription_service: SubscriptionService | None = None,
5755
cache_path: Path | None = None,
56+
job_timeout: float = 1800,
5857
) -> None:
5958
"""Initialize the job executor.
6059
@@ -69,6 +68,7 @@ def __init__(
6968
download_ugc: Whether to download UGC tracks to _Unofficial folder.
7069
subscription_service: Optional service to update subscription metadata.
7170
cache_path: Optional directory for extraction cache.
71+
job_timeout: Maximum execution time per job in seconds.
7272
"""
7373
self._job_store = job_store
7474
self._base_path = base_path
@@ -80,6 +80,7 @@ def __init__(
8080
self._download_ugc = download_ugc
8181
self._subscription_service = subscription_service
8282
self._cache_path = cache_path
83+
self._job_timeout = job_timeout
8384

8485
# Track background tasks to prevent GC during execution
8586
self._background_tasks: set[asyncio.Task[Any]] = set()
@@ -184,7 +185,7 @@ async def _run_job(
184185
if cancel_token.is_cancelled:
185186
return
186187

187-
async with asyncio.timeout(self.TIMEOUT_SECONDS):
188+
async with asyncio.timeout(self._job_timeout):
188189
self._job_store.transition(
189190
job_id,
190191
JobStatus.FETCHING_INFO,
@@ -273,7 +274,7 @@ def on_progress(
273274

274275
except TimeoutError:
275276
logger.warning(
276-
"Job %s timed out after %d seconds", job_id[:8], self.TIMEOUT_SECONDS
277+
"Job %s timed out after %d seconds", job_id[:8], self._job_timeout
277278
)
278279
cancel_token.cancel()
279280
self._job_store.transition(job_id, JobStatus.FAILED)

packages/api/src/yubal_api/settings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ class Settings(BaseSettings):
108108
description="Cron expression for scheduled sync",
109109
)
110110

111+
# Job execution
112+
job_timeout_seconds: int = Field(
113+
default=1800,
114+
ge=60,
115+
description="Job execution timeout in seconds",
116+
)
117+
111118
# Timezone
112119
tz: Timezone = Field(default="UTC", description="Timezone for timestamps")
113120

packages/api/tests/test_job_executor.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,7 @@ def store(self) -> FakeJobStore:
5353

5454
@pytest.fixture
5555
def executor(self, store: FakeJobStore, tmp_path: Any) -> JobExecutor:
56-
ex = JobExecutor(job_store=store, base_path=tmp_path)
57-
ex.TIMEOUT_SECONDS = 0.1 # 100ms for fast tests
58-
return ex
56+
return JobExecutor(job_store=store, base_path=tmp_path, job_timeout=0.1)
5957

6058
@pytest.mark.asyncio
6159
async def test_timeout_triggers_cancellation_and_fails_job(

0 commit comments

Comments
 (0)