feat(translate,scheduler,trace): new-key-only mode, stale PENDING protection, trace_id propagation

Translations:
- FR-045: new-key-only execution mode — translate only rows with unseen keys
- Compare source row keys against TranslationRecord.source_data from last succeeded run
- Baseline expired (>90 days) fallback to full mode with baseline_expired event
- run_noop early return when zero new keys (skip LLM + SQL)

Scheduler reliability:
- Stale PENDING protection: runs older than 1h auto-marked FAILED, no longer block schedule
- load_schedules() reloads active translation schedules from DB on restart
- add_translation_job/remove_translation_job register/unregister with APScheduler
- execution_mode column on translation_schedules with additive DB migration

Automation view:
- GET /settings/automation/translation-schedules endpoint
- Translation schedules displayed on Automation page below validation policies

Trace propagation:
- seed_trace_id() in all background entry points:
  TaskManager._run_task/_flusher_loop, SchedulerService._trigger_backup,
  websocket_endpoint, IdMappingService, all standalone scripts

Migrations:
- dictionary_entries: origin_run_id, origin_row_key, origin_user_id
- translation_schedules: execution_mode

Tests:
- 3 new test modules (22 tests): core_scheduler, executor_filter, scheduler_execution+guard
- Fix pre-existing translate test isolation (conftest.py)
- Fix test_list_runs_filter_status parameter
This commit is contained in:
2026-05-14 10:13:56 +03:00
parent d1695fe536
commit 8d0bc6fb93
26 changed files with 1525 additions and 19 deletions

View File

@@ -44,6 +44,7 @@ async def get_schedule(
"cron_expression": schedule.cron_expression,
"timezone": schedule.timezone,
"is_active": schedule.is_active,
"execution_mode": schedule.execution_mode,
"last_run_at": schedule.last_run_at.isoformat() if schedule.last_run_at else None,
"next_run_at": schedule.next_run_at.isoformat() if schedule.next_run_at else None,
"created_by": schedule.created_by,
@@ -77,12 +78,14 @@ async def set_schedule(
existing = db.query(TranslationSchedule).filter(
TranslationSchedule.job_id == job_id
).first()
execution_mode = getattr(payload, 'execution_mode', 'full')
if existing:
schedule = scheduler.update_schedule(
job_id,
cron_expression=payload.cron_expression,
timezone_str=payload.timezone,
is_active=payload.is_active,
execution_mode=execution_mode,
)
else:
schedule = scheduler.create_schedule(
@@ -90,6 +93,7 @@ async def set_schedule(
cron_expression=payload.cron_expression,
timezone=payload.timezone,
is_active=payload.is_active,
execution_mode=execution_mode,
)
# Register with APScheduler via SchedulerService
from ....dependencies import get_scheduler_service
@@ -99,6 +103,7 @@ async def set_schedule(
job_id=job_id,
cron_expression=schedule.cron_expression,
timezone=schedule.timezone,
execution_mode=schedule.execution_mode,
)
return {
"id": schedule.id,
@@ -106,6 +111,7 @@ async def set_schedule(
"cron_expression": schedule.cron_expression,
"timezone": schedule.timezone,
"is_active": schedule.is_active,
"execution_mode": schedule.execution_mode,
"last_run_at": schedule.last_run_at.isoformat() if schedule.last_run_at else None,
"created_by": schedule.created_by,
"created_at": schedule.created_at.isoformat() if schedule.created_at else None,
@@ -140,6 +146,7 @@ async def enable_schedule(
job_id=job_id,
cron_expression=schedule.cron_expression,
timezone=schedule.timezone,
execution_mode=schedule.execution_mode,
)
return {"status": "enabled", "job_id": job_id}
except ValueError as e: