初始提交:chongming-task 任务控制库
单任务受控执行(15 态状态机 + 审计轨迹)、DAG 管道、Controller 门面。
执行器三个:线程(默认,含 async def 任务)、内联(同步 / 测试)、进程
(spawn 子进程:硬性停止、绕开 GIL、崩溃隔离)。
调度:并发额度 + 每桶限流 + 具名资源信号量(按标签)+ 优先级排队。
持久层:TaskStore 契约 + 内存 / SQLite / Postgres 实现,契约测试随包发布。
跨进程与多节点:写穿 / 读穿、指令通道与租约、owned_buckets 划界、reconcile。
CLI、12 个可直接运行的示例、docs/ 下 5 份文档。
门禁:ruff / ruff format / mypy(strict) 干净;
pytest 563 passed(44 skipped 是需要真实 Postgres 的那批);
带一台可连的 Postgres 时 607 passed。
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
"""示例 9:无限循环任务的**启与停**。
|
||||
|
||||
一个"永远跑下去"的任务该怎么写、怎么启动、怎么停,以及三种收尾方式的终态差异。
|
||||
|
||||
运行::
|
||||
|
||||
uv run python examples/09_long_running.py
|
||||
|
||||
## 三条规则
|
||||
|
||||
1. **任务体每轮必须调 ``ctx.checkpoint()``。** 停止是**协作式**的:只有调用了它,任务
|
||||
才会看到停止请求。不调用 = 停不掉(见文件最后的对照)。
|
||||
2. **停止请求有两种收尾方式,终态不同**:
|
||||
|
||||
================================================ =========== ==========
|
||||
写法 终态 返回值
|
||||
================================================ =========== ==========
|
||||
``while True: ctx.checkpoint()``(让它抛) ``stopped`` 无
|
||||
检查 ``ctx.is_stop_requested`` 后正常 ``return`` ``succeeded`` ✅ 有
|
||||
完全不调 ``checkpoint()`` ``lost`` 无
|
||||
================================================ =========== ==========
|
||||
|
||||
"停止"在第一种写法里是"立刻中止",在第二种里是"**请收工**"——后者会把已经处理好的
|
||||
结果留下来,对"跑到下一个安全点再退出"这类需求更合适。
|
||||
3. **停止是终态。** 目前没有 ``retry``,停止/失败之后想再跑要换一个 ``task_id``
|
||||
重新提交(``create`` 会拒绝重复 id)。所以日常"先停一下"应该用 ``pause`` / ``resume``,
|
||||
``stop`` 留给"我要它彻底结束"。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from chongming_task import Controller, InMemoryTaskStore, render_table
|
||||
|
||||
|
||||
def worker(ctx: Any, source: str) -> int:
|
||||
"""无限循环:持续处理,直到收到停止请求后**优雅收尾**。"""
|
||||
processed = 0
|
||||
while not ctx.is_stop_requested:
|
||||
ctx.checkpoint()
|
||||
processed += 1
|
||||
ctx.progress(processed, unit="batch", message=f"{source} 第 {processed} 批")
|
||||
if processed % 20 == 0:
|
||||
ctx.log(f"已处理 {processed} 批")
|
||||
time.sleep(0.02)
|
||||
|
||||
# 走到这里说明收到停止请求了:做收尾,然后正常返回——
|
||||
# 返回值会被保留下来,终态是 succeeded 而不是 stopped。
|
||||
ctx.log(f"收到停止请求,收尾:本次共处理 {processed} 批")
|
||||
return processed
|
||||
|
||||
|
||||
def main() -> None:
|
||||
store = InMemoryTaskStore()
|
||||
controller = Controller(store=store, owner="node-a")
|
||||
controller.register("worker", worker)
|
||||
|
||||
# ---- 启动 ----
|
||||
run = controller.submit("worker", "kafka://orders", task_id="orders-worker")
|
||||
print(f"已启动 {run.snapshot.task_id} 状态={run.status.value}")
|
||||
time.sleep(0.5)
|
||||
|
||||
print("\n--- 运行中 ---")
|
||||
print(render_table(controller.list_tasks()))
|
||||
|
||||
# ---- 日常先停一下:pause / resume(不是 stop) ----
|
||||
print("\n--- pause(就地挂起,仍占用执行槽)---")
|
||||
print(f" pause 是否确认: {controller.pause('orders-worker', timeout=5)}")
|
||||
print(f" 状态: {run.status.value}")
|
||||
|
||||
print("\n--- resume ---")
|
||||
controller.resume("orders-worker")
|
||||
time.sleep(0.2)
|
||||
print(f" 状态: {run.status.value}")
|
||||
|
||||
# ---- 让它彻底结束:stop ----
|
||||
print("\n--- stop(终态,不可逆)---")
|
||||
controller.stop("orders-worker", reason="发布窗口")
|
||||
outcome = controller.wait("orders-worker", timeout=10)
|
||||
|
||||
print(f" 终态: {outcome.status.value}")
|
||||
print(f" 返回值: {outcome.value} ← 优雅收尾把已处理的数量带了出来")
|
||||
print("\n--- 结束后 ---")
|
||||
print(render_table(controller.list_tasks()))
|
||||
|
||||
print("\n--- 审计轨迹(谁按的、为什么,都在这里)---")
|
||||
for record in store.history("orders-worker"):
|
||||
reason = f" 原因={record.reason}" if record.reason else ""
|
||||
print(
|
||||
f" {record.version:>2} {record.from_status.value:>9} -> "
|
||||
f"{record.to_status.value:<9} {record.trigger}{reason}"
|
||||
)
|
||||
|
||||
# ---- 停止是终态:同一个 task_id 不能复活 ----
|
||||
print("\n--- 想再跑一次?同一个 task_id 会被拒绝 ---")
|
||||
try:
|
||||
controller.submit("worker", "kafka://orders", task_id="orders-worker")
|
||||
except Exception as exc:
|
||||
print(f" {type(exc).__name__}: {exc}")
|
||||
print(" 换一个 task_id 重新提交即可(或让进程一直活着,用 pause/resume 做日常启停)。")
|
||||
|
||||
controller.close(timeout=5)
|
||||
|
||||
_contrast_uncooperative()
|
||||
_print_cli_usage()
|
||||
|
||||
|
||||
def _contrast_uncooperative() -> None:
|
||||
"""对照:不调 ``checkpoint()`` 的任务**停不掉**。
|
||||
|
||||
这不是缺陷,是诚实:线程无法被强杀,所以系统只能如实报 ``lost``("结果未知"),
|
||||
而任务体仍在后台继续跑。真正终止它只能靠结束进程。
|
||||
"""
|
||||
print("\n" + "=" * 70)
|
||||
print("对照:一个从不调 checkpoint() 的无限任务")
|
||||
print("=" * 70)
|
||||
|
||||
release = threading.Event()
|
||||
ticks = {"n": 0}
|
||||
|
||||
def uncooperative(ctx: Any) -> int:
|
||||
while not release.is_set():
|
||||
ticks["n"] += 1
|
||||
time.sleep(0.02)
|
||||
return ticks["n"]
|
||||
|
||||
controller = Controller(store=InMemoryTaskStore(), graceful_stop=0.5)
|
||||
controller.register("stubborn", uncooperative)
|
||||
controller.submit("stubborn", task_id="stubborn")
|
||||
time.sleep(0.3)
|
||||
|
||||
print(" 停止请求已发出,等待载体响应……")
|
||||
controller.stop("stubborn")
|
||||
outcome = controller.wait("stubborn", timeout=8)
|
||||
print(f" 终态: {outcome.status.value} ← 不是 stopped,而是 lost(结果未知)")
|
||||
print(f" 失败类型: {outcome.failure.kind.value if outcome.failure else None}")
|
||||
|
||||
before = ticks["n"]
|
||||
time.sleep(0.4)
|
||||
alive = [t.name for t in threading.enumerate() if t.is_alive() and t.name.startswith("task-")]
|
||||
print(f" 任务体的线程还在吗: {alive}")
|
||||
print(f" 它还在继续干活吗: {ticks['n'] > before}({before} -> {ticks['n']})")
|
||||
print(" → 线程无法被强杀,只能靠结束进程来真正终止它。")
|
||||
|
||||
release.set()
|
||||
controller.close(timeout=3)
|
||||
|
||||
|
||||
def _print_cli_usage() -> None:
|
||||
"""运维视角:命令行怎么启、怎么停。"""
|
||||
print("\n" + "=" * 70)
|
||||
print("命令行(另一个进程,通过共享存储启停)")
|
||||
print("=" * 70)
|
||||
print(
|
||||
"""
|
||||
# 启动:前台执行,交给 systemd / nohup 保持存活
|
||||
# 注意 --bucket-key:限定了 --owned-buckets 时它是**必需**的,否则任务没有归属桶、
|
||||
# 不会有任何节点认领它(CLI 会在提交时报错)。
|
||||
chongming-task --db /var/lib/tasks.db --num-buckets 64 --owned-buckets 0-15 \\
|
||||
run --tasks myapp.tasks worker --arg source=kafka://orders \\
|
||||
--task-id orders-worker --bucket-key orders
|
||||
|
||||
# 另一个终端:看状态、停止
|
||||
chongming-task --db /var/lib/tasks.db --owned-buckets 0-15 ps --all --mine
|
||||
chongming-task --db /var/lib/tasks.db --owned-buckets 0-15 status orders-worker
|
||||
chongming-task --db /var/lib/tasks.db --owned-buckets 0-15 stop orders-worker --reason 发布窗口
|
||||
|
||||
# Ctrl-C 也可以:main() 会走 finally 里的 controller.close(),把在跑的任务停干净
|
||||
""".rstrip()
|
||||
)
|
||||
print(" 注意:`run` 是前台进程——它退出时任务就没人管了,请交给进程管理器。")
|
||||
sys.stdout.write("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user