feat: run stale-job recovery periodically

Co-authored-by: Copilot App <[email protected]>
This commit is contained in:
Jim Lancaster
2026-08-23 18:28:21 -05:00
co-authored by Copilot App
parent 736d0c06f4
commit f193b2800b
7 changed files with 63 additions and 3 deletions
+1
View File
@@ -167,6 +167,7 @@ class TestWorkerReliabilitySettings:
"""worker retry settings default to no retries."""
settings = _make_settings()
assert settings.worker_max_retries == 0
assert settings.worker_stale_job_seconds == 30.0
def test_provider_timeout_is_not_capped_at_twenty_seconds():
+1
View File
@@ -167,6 +167,7 @@ def test_env_example_default_values_match_settings_defaults():
"DATABASE_BACKUP_DIR": str(defaults.database_backup_dir),
"WORKER_MAX_RETRIES": str(defaults.worker_max_retries),
"WORKER_PROVIDER_TIMEOUT_SECONDS": str(defaults.worker_provider_timeout_seconds),
"WORKER_STALE_JOB_SECONDS": str(defaults.worker_stale_job_seconds),
"WORKER_MIN_TRANSCRIPTION_CHARS": str(defaults.worker_min_transcription_chars),
"WORKER_MIN_TRANSCRIPTION_LINES": str(defaults.worker_min_transcription_lines),
"WORKER_FAIL_ON_FINISH_REASON_LENGTH": str(defaults.worker_fail_on_finish_reason_length).lower(),
+45
View File
@@ -1,9 +1,15 @@
import asyncio
import logging
from datetime import UTC
from datetime import datetime
from datetime import timedelta
from typing import cast
import pytest
from sqlalchemy.ext.asyncio import async_sessionmaker
from sqlmodel.ext.asyncio.session import AsyncSession
from transcription.config import Settings
from transcription.errors import AppError
from transcription.errors import ErrorCategory
from transcription.services import ServiceBundle
@@ -79,6 +85,45 @@ async def test_run_worker_loop_survives_retriable_exception(monkeypatch, caplog)
assert worker_health.snapshot().state == "stopped"
@pytest.mark.asyncio
async def test_run_worker_loop_periodically_requeues_stale_processing_jobs(monkeypatch):
stop_event = asyncio.Event()
stale_sweep_calls: list[datetime] = []
class _Jobs:
settings = Settings(openrouter_api_key="test-key", worker_stale_job_seconds=120.0)
async def requeue_stale_processing_jobs(self, *, stale_before, session=None):
_ = session
stale_sweep_calls.append(stale_before)
return 1
class _Bundle:
jobs = _Jobs()
async def aclose(self):
return
monkeypatch.setattr(
"transcription.worker.ServiceBundle.from_session_factory",
classmethod(lambda _cls, _factory=None, **_kwargs: cast(ServiceBundle, _Bundle())),
)
async def _fake_process_next_queued_job(*, session=None, session_factory=None, services=None):
_ = (session, session_factory, services)
stop_event.set()
return False
monkeypatch.setattr("transcription.worker.process_next_queued_job", _fake_process_next_queued_job)
fake_session_factory = cast(async_sessionmaker[AsyncSession], object())
await run_worker_loop(stop_event=stop_event, poll_interval_seconds=0, session_factory=fake_session_factory)
assert len(stale_sweep_calls) >= 1
expected_upper_bound = datetime.now(UTC) - timedelta(seconds=120.0)
assert stale_sweep_calls[0] <= expected_upper_bound
@pytest.mark.asyncio
async def test_run_worker_loop_reuses_one_bundle_across_jobs(monkeypatch):
"""HIGH-02: the provider client is built once per loop, not once per job."""