Files
ss-tools/backend/src/plugins/translate/orchestrator.py
busya 339f4e0695 fix: QA issues — composite index, anchors, fallbacks
- Add composite index ix_translation_records_run_source_hash
  for NOT EXISTS dedup subquery performance + Alembic migration
- Remove duplicate #endregion in orchestrator.py (INV_3)
- Replace hardcoded RU fallback 'Статус' with 'Status'
- Add early return guard to loadMoreRecords()
- Show records summary always when recordsTotal > 0
2026-06-04 13:33:17 +03:00

176 lines
7.8 KiB
Python

# #region TranslationOrchestrator [C:5] [TYPE Module] [SEMANTICS sqlalchemy, tenacity, translate, orchestration, batch]
# @BRIEF Run lifecycle coordination: validate preconditions, dispatch executor, generate SQL, submit to Superset, record events.
# @LAYER Domain
# @RELATION DEPENDS_ON -> [TranslationRun]
# @RELATION DEPENDS_ON -> [TranslationJob]
# @RELATION DEPENDS_ON -> [TranslationExecutor]
# @RELATION DEPENDS_ON -> [SQLGenerator]
# @RELATION DEPENDS_ON -> [SupersetSqlLabExecutor]
# @RELATION DEPENDS_ON -> [TranslationEventLog]
# @RELATION DEPENDS_ON -> [TranslationPreviewSession]
# @PRE Valid job and accepted preview (for manual runs). Superset and LLM are reachable.
# @POST Translation run is executed, SQL generated and submitted, events recorded.
# @SIDE_EFFECT Creates TranslationRun; creates batches and records; generates SQL; submits to Superset; records events.
# @DATA_CONTRACT Input[db, config_manager, current_user] -> Output[TranslationRun]
# @INVARIANT State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED. Exactly one terminal event per run.
# @RATIONALE C5 orchestrator because preview, execution, event logging, and retry share state within single transaction boundary.
# @REJECTED Distributed actor model (Celery) — eventual-consistency challenges at current scale.
# @REJECTED stdout-only logging — lacks structured payload integrity; cannot enforce terminal-event invariant.
from collections.abc import Callable
from typing import Any
from sqlalchemy.orm import Session
from ...core.config_manager import ConfigManager
from ...core.logger import belief_scope
from ...models.translate import (
TranslationRun,
)
from .events import TranslationEventLog
from .orchestrator_aggregator import TranslationResultAggregator
from .orchestrator_planner import TranslationPlanner
from .orchestrator_runner import TranslationStageRunner
# #region TranslationOrchestrator [C:5] [TYPE Class]
# @BRIEF Coordinates full translation run lifecycle: validation, execution, SQL generation, Superset submission, event logging.
# @PRE DB session and config manager are available.
# @POST Runs are created, executed, and finalized with event records.
# @SIDE_EFFECT Creates/modifies TranslationRun, TranslationBatch, TranslationRecord, TranslationEvent rows.
# @INVARIANT State transitions: PENDING -> RUNNING -> COMPLETED|FAILED|CANCELLED.
# @RATIONALE Delegates to TranslationPlanner, TranslationStageRunner, TranslationResultAggregator for SOLID decomposition.
# @REJECTED Monolithic class was rejected — violated INV_7 (single contract < 150 lines, module < 400 lines).
class TranslationOrchestrator:
"""Coordinates full translation run lifecycle via delegated sub-components."""
def __init__(
self,
db: Session,
config_manager: ConfigManager,
current_user: str | None = None,
):
self.db = db
self.config_manager = config_manager
self.current_user = current_user
self.event_log = TranslationEventLog(db)
self._planner = TranslationPlanner(db, self.event_log, current_user)
self._runner = TranslationStageRunner(db, config_manager, self.event_log, current_user)
self._aggregator = TranslationResultAggregator(db, self.event_log)
# region start_run [TYPE Function]
# @PURPOSE: Start a new translation run for a job.
# @PRE job_id exists. For manual runs, an accepted preview session must exist.
# @POST TranslationRun is created in PENDING status with hash fields and config snapshot.
# @SIDE_EFFECT DB writes.
def start_run(
self,
job_id: str,
is_scheduled: bool = False,
trigger_type: str | None = None,
full_translation: bool = False,
) -> TranslationRun:
with belief_scope("TranslationOrchestrator.start_run"):
return self._planner.plan_run(
job_id=job_id,
is_scheduled=is_scheduled,
trigger_type=trigger_type,
full_translation=full_translation,
)
# endregion start_run
# region execute_run [TYPE Function]
# @PURPOSE: Execute a translation run: dispatch executor, generate SQL, submit to Superset.
# @PRE run is in PENDING status.
# @POST Run is executed, SQL generated, Superset submission attempted.
# @SIDE_EFFECT LLM calls, DB writes, Superset API calls.
def execute_run(
self,
run: TranslationRun,
on_batch_progress: Callable[[str, int, int, int, int], None] | None = None,
skip_insert: bool = False,
) -> TranslationRun:
with belief_scope("TranslationOrchestrator.execute_run"):
return self._runner.execute_run(
run=run,
on_batch_progress=on_batch_progress,
skip_insert=skip_insert,
)
# endregion execute_run
# region retry_failed_batches [TYPE Function]
# @PURPOSE: Retry failed batches in a run.
def retry_failed_batches(self, run_id: str) -> TranslationRun:
return self._runner.retry_failed_batches(run_id)
# endregion retry_failed_batches
# region retry_insert [TYPE Function]
# @PURPOSE: Retry the SQL insert phase for a completed run.
def retry_insert(self, run_id: str) -> TranslationRun:
return self._runner.retry_insert(run_id)
# endregion retry_insert
# region cancel_run [TYPE Function]
# @PURPOSE: Cancel a running translation.
def cancel_run(self, run_id: str) -> TranslationRun:
return self._runner.cancel_run(run_id)
# endregion cancel_run
# region _generate_and_insert_sql [TYPE Function] [SEMANTICS backward-compat wrapper]
# @PURPOSE: Backward-compatible delegating wrapper for SQL generation and insert.
# @SIDE_EFFECT Delegates to SQLInsertService. May call Superset API.
def _generate_and_insert_sql(
self,
job: Any,
run: TranslationRun,
) -> dict[str, Any]:
"""Backward-compatible wrapper — delegates to SQLInsertService."""
from .orchestrator_sql import SQLInsertService
svc = SQLInsertService(self.db, self.config_manager, self.event_log)
return svc.generate_and_insert_sql(job, run)
# endregion _generate_and_insert_sql
# region _update_language_stats [TYPE Function] [SEMANTICS backward-compat wrapper]
# @PURPOSE: Backward-compatible delegating wrapper for language stats update.
# @SIDE_EFFECT Delegates to TranslationResultAggregator.update_language_stats.
def _update_language_stats(
self,
run_id: str,
language_stats_map: dict[str, Any],
) -> None:
"""Backward-compatible wrapper — delegates to TranslationResultAggregator."""
return self._aggregator.update_language_stats(run_id, language_stats_map)
# endregion _update_language_stats
# region get_run_status [TYPE Function]
# @PURPOSE: Get run status with statistics.
def get_run_status(self, run_id: str) -> dict[str, Any]:
return self._aggregator.get_run_status(run_id)
# endregion get_run_status
# region get_run_records [TYPE Function]
# @PURPOSE: Get paginated records for a run.
def get_run_records(
self,
run_id: str,
page: int = 1,
page_size: int = 50,
status_filter: str | None = None,
deduplicate: bool = False,
) -> dict[str, Any]:
return self._aggregator.get_run_records(run_id, page, page_size, status_filter, deduplicate=deduplicate)
# endregion get_run_records
# region get_run_history [TYPE Function]
# @PURPOSE: Get run history for a job.
def get_run_history(
self,
job_id: str,
page: int = 1,
page_size: int = 20,
) -> tuple[int, list[dict[str, Any]]]:
return self._aggregator.get_run_history(job_id, page, page_size)
# endregion get_run_history
# #endregion TranslationOrchestrator