- GitManager: переработан в GitWorkspacePanel с вкладками - Добавлен GitLifecycleHeader с быстрыми действиями - RepositoryDashboardGrid: поддержка ReviewToggle, badges, env filter - HelpTooltip: универсальный компонент подсказок с тестами - GitManagerModel: доработаны экшены, добавлен isReady, loadDefaultBranch - Локализация en/ru для Git UI - tailwind: добавлен animation-delay-200 - ConfirmDialog: a11y-атрибуты для кнопок
241 lines
11 KiB
Markdown
241 lines
11 KiB
Markdown
# 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
|