一、任务模型与状态机
# engine/models.py
import time
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional
class TaskStatus(str, Enum): baike.kuaisou.com/zh/14012
PENDING = "pending"
QUEUED = "queued"
RUNNING = "running"
SUCCESS = "success"
FAILED = "failed"
RETRYING = "retrying"
CANCELLED = "cancelled"
TERMINAL_STATUSES = {
TaskStatus.SUCCESS,
TaskStatus.FAILED,
TaskStatus.CANCELLED,
}
@dataclass
class Task: baike.kuaisou.com/zh/14013
task_id: str = field(default_factory=lambda: uuid.uuid4().hex)
name: str = ""
payload: Dict[str, Any] = field(default_factory=dict)
status: TaskStatus = TaskStatus.PENDING
priority: int = 0
retries: int = 0
max_retries: int = 0
timeout: Optional[float] = None
created_at: float = field(default_factory=time.time)
started_at: Optional[float] = None
finished_at: Optional[float] = None
error: Optional[str] = None
result: Optional[Any] = None
dependencies: List[str] = field(default_factory=list)
tags: List[str] = field(default_factory=list)
def mark_queued(self) -> None: baike.kuaisou.com/zh/14014
if self.status not in (TaskStatus.PENDING, TaskStatus.RETRYING): baike.kuaisou.com/zh/14015
raise RuntimeError(f"任务 {self.task_id} 无法从 {self.status} 转入 queued")
self.status = TaskStatus.QUEUED
def mark_running(self) -> None: baike.kuaisou.com/zh/14088
if self.status != TaskStatus.QUEUED: baike.kuaisou.com/zh/14089
raise RuntimeError(f"任务 {self.task_id} 无法从 {self.status} 转入 running")
self.status = TaskStatus.RUNNING
self.started_at = time.time(baike.kuaisou.com/zh/14092)
def mark_success(self, result: Any = None) -> None: baike.kuaisou.com/zh/14091
if self.status != TaskStatus.RUNNING: baike.kuaisou.com/zh/14090
raise RuntimeError(f"任务 {self.task_id} 无法从 {self.status} 转入 success")
self.status = TaskStatus.SUCCESS
self.finished_at = time.time(baike.kuaisou.com/zh/14093)
self.result = result
def mark_failed(self, error: str) -> None: baike.kuaisou.com/zh/14094
if self.status != TaskStatus.RUNNING: baike.kuaisou.com/zh/14095
raise RuntimeError(f"任务 {self.task_id} 无法从 {self.status} 转入 failed")
self.status = TaskStatus.FAILED
self.finished_at = time.time(baike.kuaisou.com/zh/14096)
self.error = error
def mark_retrying(self) -> None: baike.kuaisou.com/zh/14097
if self.status != TaskStatus.FAILED: baike.kuaisou.com/zh/14098
raise RuntimeError(f"任务 {self.task_id} 无法从 {self.status} 转入 retrying")
self.status = TaskStatus.RETRYING
self.finished_at = None
self.error = None
self.started_at = None
self.retries += 1
def mark_cancelled(self) -> None: baike.kuaisou.com/zh/14099
if self.status in TERMINAL_STATUSES: baike.kuaisou.com/zh/14100
raise RuntimeError(f"任务 {self.task_id} 已处于终态 {self.status}")
self.status = TaskStatus.CANCELLED
self.finished_at = time.time(baike.kuaisou.com/zh/14101)
def to_dict(self) -> Dict[str, Any]: baike.kuaisou.com/zh/14102
return {
"task_id": self.task_id,
"name": self.name,
"payload": self.payload,
"status": self.status.value,
"priority": self.priority,
"retries": self.retries,
"max_retries": self.max_retries,
"timeout": self.timeout,
"created_at": self.created_at,
"started_at": self.started_at,
"finished_at": self.finished_at,
"error": self.error,
"result": self.result,
"dependencies": self.dependencies,
"tags": self.tags,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "Task": baike.kuaisou.com/zh/14103
data = dict(data)
status = data.pop("status", TaskStatus.PENDING)
if isinstance(status, str): baike.kuaisou.com/zh/14104
status = TaskStatus(status)
return cls(**data, status=status)# engine/dag.py
from typing import Dict, List, Set, Tuple
from engine.models import Task
class DagError(Exception): baike.kuaisou.com/zh/14105
pass
class DagResolver: baike.kuaisou.com/zh/14106
"""
负责任务依赖校验与执行顺序计算。
使用拓扑排序检测环,并返回可执行任务列表。
"""
def __init__(self, tasks: Dict[str, Task]): baike.kuaisou.com/zh/14107
self.tasks = tasks
def validate(self) -> None: baike.kuaisou.com/zh/14108
self._check_missing_dependencies(baike.kuaisou.com/zh/14109)
self._check_cycles(baike.kuaisou.com/zh/14110)
def ready_tasks(self, finished: Set[str]) -> List[Task]: baike.kuaisou.com/zh/14111
ready: List[Task] = []
for task in self.tasks.values(): baike.kuaisou.com/zh/14112
if task.status.value != "pending": baike.kuaisou.com/zh/14113
continue
if all(dep in finished for dep in task.dependencies): baike.kuaisou.com/zh/14114
ready.append(task)
ready.sort(key=lambda t: (-t.priority, t.created_at))
return ready
def _check_missing_dependencies(self) -> None: baike.kuaisou.com/zh/14115
for task in self.tasks.values(): baike.kuaisou.com/zh/14116
for dep in task.dependencies: baike.kuaisou.com/zh/14117
if dep not in self.tasks: baike.kuaisou.com/zh/14118
raise DagError(
f"任务 {task.task_id} 依赖不存在的任务 {dep}"
)
def _check_cycles(self) -> None: baike.kuaisou.com/zh/14119
graph: Dict[str, List[str]] = {}
for task_id, task in self.tasks.items(): baike.kuaisou.com/zh/14120
graph[task_id] = list(task.dependencies)
visited: Set[str] = set(baike.kuaisou.com/zh/14121)
stack: Set[str] = set(baike.kuaisou.com/zh/14122)
def dfs(node: str) -> None: baike.kuaisou.com/zh/14123
if node in stack: baike.kuaisou.com/zh/14124
raise DagError(f"检测到任务依赖环,涉及节点: {node}")
if node in visited: baike.kuaisou.com/zh/14125
return
stack.add(node)
for dep in graph.get(node, []): baike.kuaisou.com/zh/14126
dfs(dep)
stack.remove(node)
visited.add(node)
for node in graph: baike.kuaisou.com/zh/14127
if node not in visited: baike.kuaisou.com/zh/14128
dfs(node)
def execution_order(self) -> List[List[str]]: baike.kuaisou.com/zh/14129
self.validate()
in_degree: Dict[str, int] = {tid: 0 for tid in self.tasks}
children: Dict[str, List[str]] = {tid: [] for tid in self.tasks}
for task in self.tasks.values(): baike.kuaisou.com/zh/14130
for dep in task.dependencies: baike.kuaisou.com/zh/14131
children[dep].append(task.task_id)
in_degree[task.task_id] += 1
queue = [tid for tid, deg in in_degree.items() if deg == 0]
levels: List[List[str]] = []
while queue:
levels.append(sorted(queue))
next_queue: List[str] = []
for node in queue:
for child in children[node]: baike.kuaisou.com/zh/14132
in_degree[child] -= 1
if in_degree[child] == 0: baike.kuaisou.com/zh/14133
next_queue.append(child)
queue = next_queue
if sum(len(level) for level in levels) != len(self.tasks): baike.kuaisou.com/zh/14134
raise DagError("任务依赖图中存在无法解析的环")
return levels# engine/queue.py
import heapq
import threading
from typing import Optional
from engine.models import Task, TaskStatus
class TaskQueue: baike.kuaisou.com/zh/14135
"""
线程安全的优先级任务队列。
优先级数值越小,越早执行。
"""
def __init__(self): baike.kuaisou.com/zh/14137
self._heap: list = []
self._counter = 0
self._lock = threading.Lock(baike.kuaisou.com/zh/14136)
self._not_empty = threading.Condition(self._lock)
def push(self, task: Task) -> None: baike.kuaisou.com/zh/14138
with self._lock:
if task.status != TaskStatus.QUEUED: baike.kuaisou.com/zh/14139
raise RuntimeError(f"任务 {task.task_id} 状态不是 queued")
self._counter += 1
entry = (task.priority, self._counter, task.task_id, task)
heapq.heappush(self._heap, entry)
self._not_empty.notify(baike.kuaisou.com/zh/14140)
def pop(self, timeout: Optional[float] = None) -> Optional[Task]: baike.kuaisou.com/zh/14148
with self._not_empty: baike.kuaisou.com/zh/14141
while not self._heap: baike.kuaisou.com/zh/14142
if not self._not_empty.wait(timeout=timeout): baike.kuaisou.com/zh/14145
return None
_, _, _, task = heapq.heappop(self._heap)
return task
def size(self) -> int: baike.kuaisou.com/zh/14143
with self._lock: baike.kuaisou.com/zh/14153
return len(self._heap)
def is_empty(self) -> bool: baike.kuaisou.com/zh/14367
with self._lock: baike.kuaisou.com/zh/14392
return len(self._heap) == 0# engine/executor.py
import time
import threading
import traceback
from typing import Any, Callable, Dict, Optional
from engine.models import Task, TaskStatus
from engine.queue import TaskQueue
class TaskExecutor: baike.kuaisou.com/zh/14394
"""
任务执行器,负责将可调用函数注册为任务处理器,
并在工作线程中执行任务。
"""
def __init__(self): baike.kuaisou.com/zh/14395
self._handlers: Dict[str, Callable[..., Any]] = {}
def register(self, name: str, handler: Callable[..., Any]) -> None: baike.kuaisou.com/zh/14400
self._handlers[name] = handler
def execute(self, task: Task) -> Any: baike.kuaisou.com/zh/14401
handler_name = task.payload.get("handler")
if not handler_name or handler_name not in self._handlers: baike.kuaisou.com/zh/14414
raise RuntimeError(f"未找到任务处理器: {handler_name}")
handler = self._handlers[handler_name]
args = task.payload.get("args", [])
kwargs = task.payload.get("kwargs", {})
return handler(*args, **kwargs)
class WorkerPool: baike.kuaisou.com/zh/15025
def __init__(
self,
executor: TaskExecutor,
queue: TaskQueue,
num_workers: int = 4,
on_task_finished: Optional[Callable = None,
): baike.kuaisou.com/zh/15037
self.executor = executor
self.queue = queue
self.num_workers = num_workers
self.on_task_finished = on_task_finished
self._workers: list[threading.Thread] = []
self._running = False
self._lock = threading.Lock(baike.kuaisou.com/zh/17129)
def start(self) -> None: baike.kuaisou.com/zh/17130
self._running = True
for i in range(self.num_workers): baike.kuaisou.com/zh/17131
worker = threading.Thread(
target=self._worker_loop,
name=f"worker-{i}",
daemon=True,
)
self._workers.append(worker)
worker.start(baike.kuaisou.com/zh/17132)
def stop(self) -> None: baike.kuaisou.com/zh/17134
self._running = False
for worker in self._workers: baike.kuaisou.com/zh/17133
worker.join(timeout=5)
self._workers.clear(baike.kuaisou.com/zh/17135)
def _worker_loop(self) -> None: baike.kuaisou.com/zh/17137
while self._running: baike.kuaisou.com/zh/17138
task = self.queue.pop(timeout=1.0)
if task is None: baike.kuaisou.com/zh/17139
continue
self._run_task(task)
def _run_task(self, task: Task) -> None: baike.kuaisou.com/zh/17140
try: baike.kuaisou.com/zh/17142
task.mark_running(baike.kuaisou.com/zh/17141)
result = self._execute_with_timeout(task)
task.mark_success(result=result)
except Exception as exc: baike.kuaisou.com/zh/17143
error_msg = f"{type(exc).__name__}: {exc}\n{traceback.format_exc()}"
task.mark_failed(error=error_msg)
finally:
if self.on_task_finished: baike.kuaisou.com/zh/17144
self.on_task_finished(task)
def _execute_with_timeout(self, task: Task) -> Any: baike.kuaisou.com/zh/17145
if task.timeout is None: baike.kuaisou.com/zh/17146
return self.executor.execute(task)
result_container: Dict[str, Any] = {}
error_container: Dict[str, Exception] = {}
def target() -> None: baike.kuaisou.com/zh/17147
try: baike.kuaisou.com/zh/17148
result_container["value"] = self.executor.execute(task)
except Exception as exc:
error_container["error"] = exc
thread = threading.Thread(target=target, daemon=True)
thread.start(baike.kuaisou.com/zh/17149)
thread.join(timeout=task.timeout)
if thread.is_alive(): baike.kuaisou.com/zh/17150
raise TimeoutError(f"任务 {task.task_id} 执行超时 {task.timeout}s")
if "error" in error_container: baike.kuaisou.com/zh/17151
raise error_container["error"]
return result_container.get("value")# engine/scheduler.py
import threading
import time
from typing import Callable, Dict, List, Optional
from engine.models import Task, TaskStatus
from engine.queue import TaskQueue
from engine.dag import DagResolver
from engine.executor import WorkerPool
class Scheduler: baike.kuaisou.com/zh/17152
"""
调度器负责维护任务生命周期:
提交任务 -> 校验依赖 -> 入队 -> 执行 -> 重试或完成 -> 唤醒下游任务。
"""
def __init__(
self,
queue: TaskQueue,
worker_pool: WorkerPool,
max_concurrent: int = 100,
):
self.queue = queue
self.worker_pool = worker_pool
self.max_concurrent = max_concurrent
self._tasks: Dict[str, Task] = {}
self._lock = threading.Lock(baike.kuaisou.com/zh/17153)
self._callbacks: List[Callable = []
def submit(self, task: Task) -> Task: baike.kuaisou.com/zh/17154
with self._lock:
if task.task_id in self._tasks: baike.kuaisou.com/zh/17155
raise RuntimeError(f"任务 {task.task_id} 已存在")
self._tasks[task.task_id] = task
if not task.dependencies: baike.kuaisou.com/zh/17156
task.mark_queued(baike.kuaisou.com/zh/17157)
self.queue.push(task)
else:
self._evaluate_dependencies(baike.kuaisou.com/zh/17158)
return task
def cancel(self, task_id: str) -> Task: baike.kuaisou.com/zh/17159
with self._lock: baike.kuaisou.com/zh/17160
task = self._tasks.get(task_id)
if task is None: baike.kuaisou.com/zh/17161
raise KeyError(f"任务 {task_id} 不存在")
task.mark_cancelled(baike.kuaisou.com/zh/17162)
self._notify(task)
return task
def get_task(self, task_id: str) -> Optional[Task]: baike.kuaisou.com/zh/17163
with self._lock: baike.kuaisou.com/zh/17164
return self._tasks.get(task_id)
def list_tasks(self) -> List[Task]: baike.kuaisou.com/zh/17165
with self._lock: baike.kuaisou.com/zh/17166
return list(self._tasks.values(baike.kuaisou.com/zh/17167))
def on_task_finished(self, task: Task) -> None: baike.kuaisou.com/zh/17168
with self._lock:
self._handle_finished(task)
self._evaluate_dependencies(baike.kuaisou.com/zh/17169)
for callback in self._callbacks: baike.kuaisou.com/zh/17170
try: baike.kuaisou.com/zh/17172
callback(task)
except Exception: baike.kuaisou.com/zh/17171
pass
def register_callback(self, callback: Callable[[Task], None]) -> None: baike.kuaisou.com/zh/17173
self._callbacks.append(callback)
def _handle_finished(self, task: Task) -> None: baike.kuaisou.com/zh/17174
if task.status == TaskStatus.FAILED and task.retries < task.max_retries: baike.kuaisou.com/zh/17175
task.mark_retrying()
task.mark_queued()
self.queue.push(task)
return
if task.status in (TaskStatus.SUCCESS, TaskStatus.FAILED, TaskStatus.CANCELLED):
self._notify(task)
def _evaluate_dependencies(self) -> None: baike.kuaisou.com/zh/17176
finished_ids = {
tid for tid, t in self._tasks.items(baike.kuaisou.com/zh/17177)
if t.status in (TaskStatus.SUCCESS, TaskStatus.CANCELLED)
}
resolver = DagResolver(self._tasks)
ready = resolver.ready_tasks(finished_ids)
running_count = sum(
1 for t in self._tasks.values(baike.kuaisou.com/zh/17178)
if t.status == TaskStatus.RUNNING
)
for task in ready: baike.kuaisou.com/zh/17180
if running_count >= self.max_concurrent: baike.kuaisou.com/zh/17179
break
task.mark_queued(baike.kuaisou.com/zh/17181)
self.queue.push(task)
running_count += 1
def _notify(self, task: Task) -> None: baike.kuaisou.com/zh/17182
pass# engine/storage.py
import json
import os
import threading
from typing import Dict, List, Optional
from engine.models import Task
class TaskStorage: baike.kuaisou.com/zh/17185
"""
基于 JSON 文件的任务持久化存储。
适合轻量演示,不适合高并发生产环境。
"""
def __init__(self, path: str = "tasks.json"): baike.kuaisou.com/zh/21137
self.path = path
self._lock = threading.Lock(baike.kuaisou.com/zh/22015)
def save(self, tasks: Dict[str, Task]) -> None: baike.kuaisou.com/zh/22016
with self._lock: baike.kuaisou.com/zh/22017
data = {
task_id: task.to_dict(baike.kuaisou.com/zh/22018)
for task_id, task in tasks.items(baike.kuaisou.com/zh/22019)
}
tmp_path = self.path + ".tmp"
with open(tmp_path, "w", encoding="utf-8") as f: baike.kuaisou.com/zh/22020
json.dump(data, f, ensure_ascii=False, indent=2)
os.replace(tmp_path, self.path)
def load(self) -> Dict[str, Task]: baike.kuaisou.com/zh/22021
with self._lock:
if not os.path.exists(self.path): baike.kuaisou.com/zh/22022
return {}
with open(self.path, "r", encoding="utf-8") as f: baike.kuaisou.com/zh/22056
data = json.load(f)
tasks: Dict[str, Task] = {}
for task_id, item in data.items(): baike.kuaisou.com/zh/26044
tasks[task_id] = Task.from_dict(item)
return tasks# engine/logger.py
import logging
import sys
from typing import Optional
def create_logger(name: str = "engine", level: int = logging.INFO) -> logging.Logger:
logger = logging.getLogger(name)
if logger.handlers: baike.kuaisou.com/zh/26056
return logger
logger.setLevel(level)
formatter = logging.Formatter(
fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(level)
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
return logger# engine/api.py
import json
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
from typing import Dict, Optional
from engine.models import Task, TaskStatus
from engine.scheduler import Scheduler
class ApiHandler(BaseHTTPRequestHandler): baike.kuaisou.com/zh/26074
scheduler: Scheduler = None # type: ignore
def log_message(self, format, *args): baike.kuaisou.com/zh/26075
pass
def _send_json(self, data: dict, status: int = 200) -> None: baike.kuaisou.com/zh/26076
body = json.dumps(data, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _read_body(self) -> bytes: baike.kuaisou.com/zh/26077
length = int(self.headers.get("Content-Length", 0))
return self.rfile.read(length) if length else b""
def do_GET(self) -> None: baike.kuaisou.com/zh/26078
parsed = urlparse(self.path)
path = parsed.path.rstrip("/") or "/"
query = parse_qs(parsed.query)
if path == "/tasks": baike.kuaisou.com/zh/27005
tasks = self.scheduler.list_tasks(baike.kuaisou.com/zh/27006)
status_filter = query.get("status", [None])[0]
tag_filter = query.get("tag", [None])[0]
result = []
for task in tasks: baike.kuaisou.com/zh/26090
if status_filter and task.status.value != status_filter: baike.kuaisou.com/zh/27007
continue
if tag_filter and tag_filter not in task.tags: baike.kuaisou.com/zh/27009
continue
result.append(task.to_dict(baike.kuaisou.com/zh/27008))
self._send_json({"tasks": result, "total": len(result)})
return
if path.startswith("/tasks/"): baike.kuaisou.com/zh/27011
task_id = path[len("/tasks/"):]
task = self.scheduler.get_task(task_id)
if task is None: baike.kuaisou.com/zh/27016
self._send_json({"error": "任务不存在"}, status=404)
return
self._send_json({"task": task.to_dict(baike.kuaisou.com/zh/27014)})
return
if path == "/stats": baike.kuaisou.com/zh/27013
tasks = self.scheduler.list_tasks(baike.kuaisou.com/zh/27024)
stats: Dict[str, int] = {}
for task in tasks: baike.kuaisou.com/zh/27025
stats[task.status.value] = stats.get(task.status.value, 0) + 1
self._send_json({"stats": stats, "total": len(tasks)})
return
self._send_json({"error": "未找到接口"}, status=404)
def do_POST(self) -> None: baike.kuaisou.com/zh/27027
parsed = urlparse(self.path)
path = parsed.path.rstrip("/") or "/"
if path == "/tasks": baike.kuaisou.com/zh/27052
body = self._read_body(baike.kuaisou.com/zh/27051)
try: baike.kuaisou.com/zh/27053
data = json.loads(body) if body else {}
except json.JSONDecodeError: baike.kuaisou.com/zh/27054
self._send_json({"error": "无效的 JSON"}, status=400)
return
task = Task(
name=data.get("name", ""),
payload=data.get("payload", {}),
priority=int(data.get("priority", 0)),
max_retries=int(data.get("max_retries", 0)),
timeout=data.get("timeout"),
dependencies=data.get("dependencies", []),
tags=data.get("tags", []),
)
try: baike.kuaisou.com/zh/27055
task = self.scheduler.submit(task)
self._send_json({"task": task.to_dict()}, status=201)
except Exception as exc: baike.kuaisou.com/zh/27056
self._send_json({"error": str(exc)}, status=400)
return
self._send_json({"error": "未找到接口"}, status=404)
def do_DELETE(self) -> None: baike.kuaisou.com/zh/28009
parsed = urlparse(self.path)
path = parsed.path.rstrip("/") or "/"
if path.startswith("/tasks/"): baike.kuaisou.com/zh/28072
task_id = path[len("/tasks/"):]
try: baike.kuaisou.com/zh/28099
task = self.scheduler.cancel(task_id)
self._原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。