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
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
Translation scheduling flow (also bypasses)
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
If translation were unified:
- Create
TranslatePlugin extends PluginBase with id = "translate-run" or similar
- 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)
- The scheduler would call
task_manager.create_task("translate-run", {schedule_id, job_id}) instead of execute_scheduled_translation()
- Manual POST would call
task_manager.create_task() instead of the orchestrator directly
- 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