Files
ss-tools/specs/research-task-execution-architecture.md
busya 61f3e6db75 feat: Git manager UI — панель управления Git + HelpTooltip + ReviewToggle
- GitManager: переработан в GitWorkspacePanel с вкладками
- Добавлен GitLifecycleHeader с быстрыми действиями
- RepositoryDashboardGrid: поддержка ReviewToggle, badges, env filter
- HelpTooltip: универсальный компонент подсказок с тестами
- GitManagerModel: доработаны экшены, добавлен isReady, loadDefaultBranch
- Локализация en/ru для Git UI
- tailwind: добавлен animation-delay-200
- ConfirmDialog: a11y-атрибуты для кнопок
2026-07-04 15:01:45 +03:00

241 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Task Execution Architecture — superset-tools
**Date:** 2026-07-02
**Purpose:** Comprehensive audit of task execution paths — what runs through TaskManager, what bypasses it, and why.
**Context:** User noted translation tasks are missing from `/reports` (Task Status Center), which only shows tasks from the generic TaskManager pipeline.
---
## 1. TaskManager Pipeline (the unified path)
### Core files
| File | Purpose |
|------|---------|
| `src/core/task_manager/manager.py` | Thin facade composing Graph, EventBus, Lifecycle |
| `src/core/task_manager/graph.py` | In-memory Task registry with CRUD, pagination, filters |
| `src/core/task_manager/lifecycle.py` | State machine: PENDING→RUNNING→SUCCESS/FAILED/WAITING |
| `src/core/task_manager/event_bus.py` | Async log buffer, persistence flush, WebSocket fan-out |
| `src/core/task_manager/context.py` | TaskContext container passed to plugin.execute() |
| `src/core/task_manager/models.py` | Task, TaskStatus, LogEntry, LogFilter (Pydantic) |
| `src/core/plugin_loader.py` | Filesystem-based PluginBase discovery and registration |
| `src/core/plugin_base.py` | ABC PluginBase with id, name, execute, get_schema |
| `src/core/scheduler.py` | APScheduler service (backup, validation, translation jobs) |
| `src/core/async_job_runner.py` | Bridge: sync APScheduler ↔ async event loop |
| `src/dependencies.py` | Singleton factory for TaskManager, PluginLoader, SchedulerService |
| `src/api/routes/tasks.py` | REST API: POST/GET /api/tasks, WebSocket status/logs |
### Pipeline flow
```
POST /api/tasks {"plugin_id": "...", "params": {...}}
TaskManager.create_task(plugin_id, params) [manager.py:306]
JobLifecycle.create_task(plugin_id, params) [lifecycle.py:96]
├─ PluginLoader.has_plugin(plugin_id) → raise ValueError if missing
├─ Task(plugin_id=..., params=..., status=PENDING)
├─ TaskGraph.add_task(task) [graph.py:125]
├─ TaskPersistenceService.persist_task(task) [lifecycle.py:111]
└─ returns Task object
asyncio.create_task( lifecycle._run_task(task_id) ) [manager.py:314]
JobLifecycle._run_task(task_id) [lifecycle.py:128]
├─ TaskGraph.get_task(task_id)
├─ PluginLoader.get_plugin(task.plugin_id)
├─ task.status = RUNNING; persisted; broadcast_status
├─ Creates TaskContext(task_id, add_log_fn, params) [context.py:70]
├─ Inspects plugin.execute() signature:
│ └─ If accepts `context`: plugin.execute(params, context=context)
│ If sync: wrapped in asyncio.to_thread()
│ If async: awaited directly
├─ On success: task.result = result; task.status = SUCCESS
├─ On failure: task.status = FAILED
├─ Finally: task.finished_at, flush_task_logs(), persist_task(), broadcast
└─ Additional: broadcasts dataset.updated for "dataset-mapper"/"llm_documentation"
```
### TaskStatus values
- `PENDING`, `RUNNING`, `SUCCESS`, `FAILED`, `AWAITING_MAPPING`, `AWAITING_INPUT`
### Registered plugin_id values (PluginBase subclasses)
All discovered by PluginLoader scanning `backend/src/plugins/`. Classes inheriting `PluginBase` are instantiated and registered by their `id` property:
| plugin_id | PluginBase subclass | Source file |
|-----------|-------------------|-------------|
| `superset-backup` | BackupPlugin | `backup.py` |
| `superset-migration` | MigrationPlugin | `migration.py` |
| `search-datasets` | SearchPlugin | `search.py` |
| `dataset-mapper` | MapperPlugin | `mapper.py` |
| `system-debug` | DebugPlugin | `debug.py` |
| `maintenance_banner_apply` | MaintenanceBannerPlugin | `maintenance_banner.py` |
| `git-integration` | GitPlugin | `git_plugin.py` |
| `llm_dashboard_validation` | DashboardValidationPlugin | `llm_analysis/plugin.py` |
| `llm_documentation` | DocumentationPlugin | `llm_analysis/plugin.py` |
---
## 2. Scheduler Service
SchedulerService (`src/core/scheduler.py`) manages three types of jobs via APScheduler:
| Job Type | Trigger Mechanism | Uses TaskManager? |
|----------|------------------|-------------------|
| Backup (`backup_{env_id}`) | `task_manager.create_task("superset-backup")` via AsyncJobRunner.run() | **Yes** |
| Translation (`translate_{schedule_id}`) | Direct call to execute_scheduled_translation() → TranslationOrchestrator | **No** |
| Validation (`validation_{policy_id}`) | `task_manager.create_task("llm_dashboard_validation")` via AsyncJobRunner.run() | **Yes** |
---
## 3. Translation System (standalone, bypasses TaskManager)
Translation tasks use a **separate, parallel execution pipeline**. They never go through `TaskManager.create_task()` or `PluginBase.execute()`.
### Key files
| File | Purpose |
|------|---------|
| `src/plugins/translate/orchestrator.py` | TranslationOrchestrator — run lifecycle coordination |
| `src/plugins/translate/orchestrator_planner.py` | TranslationPlanner — plan generation |
| `src/plugins/translate/orchestrator_runner.py` | TranslationStageRunner — execution, retry, cancel |
| `src/plugins/translate/orchestrator_sql.py` | SQL INSERT orchestrator |
| `src/plugins/translate/scheduler.py` | TranslationScheduler CRUD + execute_scheduled_translation() |
| `src/api/routes/translate/_run_routes.py` | POST /api/translate/jobs/{job_id}/run |
| `src/api/routes/translate/_schedule_routes.py` | Translation schedule CRUD |
### Translation execution flow
```
POST /api/translate/jobs/{job_id}/run [_run_routes.py:32]
TranslationOrchestrator(db, config_manager, username) [orchestrator.py:46]
TranslationPlanner.plan_run(job_id) [orchestrator_planner.py]
├─ Creates TranslationRun DB row (status=PENDING)
└─ Returns TranslationRun object
asyncio.create_task( _background_execute() ) [_run_routes.py:123]
│ (separate DB session, separate orchestrator)
TranslationOrchestrator.execute_run(bg_run) [orchestrator.py:89]
TranslationStageRunner.execute_run(run) [orchestrator_runner.py:45]
TranslationExecutionEngine.execute_run(run)
├─ Fetches data from source
├─ Creates batches
├─ Calls LLM for translation (per-batch)
├─ Generates SQL INSERT statements
├─ Submits SQL to Superset SQL Lab
└─ Records results in TranslationRun/TranslationBatch/TranslationRecord DB rows
```
### Translation scheduling flow (also bypasses)
```
SchedulerService.load_schedules() [scheduler.py:69]
├─ Queries TranslationSchedule table for is_active=True
└─ scheduler.add_job(
execute_scheduled_translation, [translate/scheduler.py:278]
CronTrigger(...)
)
▼ (on trigger)
execute_scheduled_translation(schedule_id, job_id, ...)
├─ TranslationOrchestrator(db, config_manager, "scheduler")
├─ orch.start_run(job_id=job_id, is_scheduled=True)
├─ orch.execute_run(run) via AsyncJobRunner.run()
└─ TranslationRun.status set in DB
```
### Key differences: TaskManager vs Translation
| Feature | TaskManager (PluginBase) | Translation Runs |
|---------|------------------------|------------------|
| State model | Pydantic `Task` in memory (SQL persistence) | SQLAlchemy `TranslationRun` in DB |
| Logger | `TaskContext.logger` → EventBus → WebSocket push | `TranslationEventLog` → DB rows |
| WebSocket | `/ws/logs/{task_id}` (push) + `/ws/task-events` | `/ws/translate/run/{run_id}` (poll, 1s interval) |
| Execution model | `plugin.execute(params, context)` | `TranslationOrchestrator.execute_run(run)` |
| Pause/Resume | Built-in (AWAITING_INPUT, AWAITING_MAPPING) | Not supported |
| Cancellation | `TaskManager.cancel_task()` | `TranslationStageRunner.cancel_run()` |
| Discovery | PluginLoader filesystem scan | Hardcoded orchestrator class |
| Results in `/reports` | Yes | **No** |
---
## 4. Complete Audit: All Execution Paths Bypassing TaskManager
### 🔴 Critical bypasses (full task execution, NOT in TaskManager)
| # | Path | File:Line | Launcher | Work Done | State Tracking |
|---|------|-----------|----------|-----------|----------------|
| **A1** | Manual translation run | `_run_routes.py:123` | `asyncio.create_task(_background_execute())` | LLM translation, SQL generation, Superset API | `TranslationRun` table (SQLAlchemy) |
| **A2** | Scheduled translation run | `translate/scheduler.py:278` | APScheduler → AsyncJobRunner.run() | LLM translation, SQL generation, Superset API | `TranslationRun` table (SQLAlchemy) |
### 🟡 Semi-bypasses (blocking HTTP, could be TaskManager async)
| # | Path | File:Line | Work Done | Notes |
|---|------|-----------|-----------|-------|
| **D1** | Retry failed batches | `_run_routes.py:140` | LLM calls + SQL gen | Blocks HTTP response, no 202 Accepted |
| **D2** | Retry SQL insert | `_run_routes.py:168` | Superset SQL submit | Blocks HTTP response, no 202 Accepted |
### 🟢 Non-task execution paths (should NOT be in TaskManager)
| # | Path | File:Line | Work Done | Reason for staying out |
|---|------|-----------|-----------|----------------------|
| **A3** | Agent LLM title generation | `agent/app.py:488` | `asyncio.create_task(generate_llm_title(...))` | Best-effort, sub-second, non-critical. No business state beyond title text. |
| **B1** | EventBus async flusher | `event_bus.py:72` | Flush log buffer to DB every 2s | Internal TaskManager infrastructure |
| **B2** | TaskLogger fire-forget log writes | `task_logger.py:100` | Async log delivery to EventBus | Internal TaskManager infrastructure |
| **C1-C5** | WebSocket event consumers | `app.py:664,770,824,866,895` | Relay events to browser clients | Event relay, not task execution |
| **E** | Thread pool executors | `utils/executors.py:50` | 3× ThreadPoolExecutor | Infra for blocking I/O offloading |
---
## 5. Impact Summary
```
TaskManager (unified)
├─ backup ✅
├─ migration ✅
├─ llm_validation ✅
├─ llm_documentation ✅
├─ dataset-mapper ✅
├─ search-datasets ✅
├─ git-integration ✅
├─ maintenance ✅
├─ debug ✅
└─ translation ❌ ← bypasses completely (A1 + A2)
dataset review? need to verify
```
### If translation were unified:
1. Create `TranslatePlugin extends PluginBase` with `id = "translate-run"` or similar
2. Its `execute(params, context)` method would:
- Receive `job_id` and `run_id` from params
- Open its own DB session
- Call `TranslationOrchestrator(db, ...).execute_run(run)`
- Report progress via `context.logger` (→ automatic WebSocket push)
3. The scheduler would call `task_manager.create_task("translate-run", {schedule_id, job_id})` instead of `execute_scheduled_translation()`
4. Manual POST would call `task_manager.create_task()` instead of the orchestrator directly
5. Translation tasks would automatically appear in `/reports` with log streaming, status broadcasts, and cancel support
### Key benefit:
- Single API: `POST /api/tasks` for ALL background work
- Single monitoring: `/reports` shows ALL tasks including translations
- Unified WebSocket: push-based logs instead of poll-based
- Elimination of ~200 lines of duplicated concurrency/DB-session/stale-run cleanup code