任务模式与调度#
任务在声明时确定模式,模式决定它何时被执行:是提交即执行,还是延后一次,
还是按周期反复执行。共四种模式 —— direct、delay、cron、interval。
它们在提交链路上的位置见架构。
声明任务#
from binomic.task import task
@task("default")
def example(index: int = 0) -> None: ...
第一个位置参数是队列名,需与 BinomicConfig 的 queues 对应;mode 是仅限
关键字传参,默认 "direct"。任务以函数名的小写形式注册,投递时用该名字指定任务;
注册表内名字唯一,重复注册同名任务会抛 DuplicateTaskError。
模式 |
附加参数 |
执行时机 |
能否带参数 |
|---|---|---|---|
|
无(默认) |
提交时立即执行 |
可以 |
|
|
提交后延迟指定秒数执行一次 |
可以 |
|
|
按 cron 表达式重复执行 |
不可以 |
|
|
每隔指定秒数重复执行 |
不可以 |
# 延迟 10 秒执行一次
@task("default", mode="delay", delay=10)
def delayed() -> None: ...
# 每 5 分钟执行(cron 表达式,分/时/日/月/周)
@task("default", mode="cron", cron="*/5 * * * *")
def periodic() -> None: ...
# 每 30 秒执行
@task("default", mode="interval", interval=30)
def poll() -> None: ...
声明期会立刻校验,不合法直接抛 ValueError:
缺必需参数:
delay/cron/interval模式各自要求对应的参数;周期任务带参数:
cron与interval模式的函数不能有参数 —— 周期触发时没有调用 参数可传(原因见下节)。
延迟与间隔的取值只要求是正数,这条在提交/调度时才由调度器校验(声明期只校验 参数是否存在),见下文「直接使用调度器」。
备注
cron 与 interval 的触发时刻按调度器时区计算,默认是 UTC。客户端自动注册
周期任务时不接受时区配置,因此 cron="0 9 * * *" 指的是 UTC 9 点而非本地 9 点。
需要按其它时区触发时,用下面的 TaskScheduler(timezone=...) 自行注册。
提交任务#
Binomic.submit 把一条 Message 交给客户端处理,但只支持两种模式:
direct:立即把消息写入任务声明的队列;delay:交给调度器,延迟到期后再写入队列。
对 cron 或 interval 任务调用 submit 会抛 ValueError —— 周期任务不接受单次
提交,它们的触发由客户端在启动时自动注册(见下节)。消息进哪条 Stream 由任务声明
里的队列决定,submit 不接受调用方指定队列。
from binomic.message import Message
# `binomic` 的构造见「快速上手」
async with binomic:
await binomic.submit(Message(name="example", args=[1]))
周期任务的注册#
客户端在进入上下文(async with binomic)时,遍历注册表中所有 cron / interval
任务,为每个任务注册一个周期作业:每次触发即向该任务的队列投递一条消息。这也解释了
上一节的约束 —— 周期触发的消息不带调用参数,因此周期任务必须是无参函数。
直接使用调度器#
TaskScheduler 是公开 API,可独立于客户端使用。
import asyncio
from binomic.task import TaskScheduler, task
@task("default", mode="delay", delay=5)
def reminder_delay() -> None: ...
def send_reminder(user_id: str) -> None:
print(f"reminder for {user_id}")
async def main() -> None:
scheduler = TaskScheduler()
scheduler.start() # 必须在运行中的事件循环内
scheduler.delay(func=send_reminder, spec=reminder_delay, args=("u-1",))
await asyncio.sleep(6) # 等作业到期
scheduler.shutdown()
asyncio.run(main())
上例中 reminder_delay 只用来提供「延迟 5 秒」这一时序参数与校验,真正被调用的可调用
对象是传给 func 的 send_reminder。
必须在运行中的事件循环内使用:
TaskScheduler基于 APScheduler 的AsyncIOScheduler,后者在start()时就地捕获当前事件循环 —— 在事件循环之外调用start()会抛RuntimeError: no running event loop。三个方法本身是同步方法, 但前提是有一个正在运行的事件循环(客户端路径天然满足,因为它在事件循环内启动调度器)。构造:
TaskScheduler(timezone=UTC),默认时区为 UTC;三个方法创建的触发器都使用 该时区。spec从哪来:@task(...)装饰器的返回值就是一个TaskSpec,它提供时序参数 (delay/cron/interval)并参与校验。方法:
delay(func, spec, *, delay=None, args=None, kwargs=None)、interval(...)、cron(...)均为同步方法;func是到期时要调用的可调用对象,args/kwargs原样传给func。delay多一个可选关键字delay:给出时直接用它 作为延时秒数,并跳过「spec必须是delay模式」那一条校验 —— Worker 的失败重投 正是靠它,把一个非 delay 模式的spec排成延时作业(见可靠性)。校验:
spec的模式与所调方法不符、或缺少对应取值时抛ValueError(如Task is not a delay task or lack `delay` value.,仅当未显式给出delay时生效)。delay与interval的取值<= 0时也抛ValueError(... must be greater than 0.);cron没有非正值校验 —— cron 表达式不存在 「非正值」的概念。生命周期:
start()之后作业才会真正触发,shutdown()停机,二者均为同步方法。 注意shutdown()紧跟在调度之后调用会让作业来不及触发(上例用await asyncio.sleep(6)等到达期)。迟到也执行:排出的作业带
misfire_grace_time=None,因此即便调度器在run_date之后才轮到它(事件循环被占住、或进程一度停顿),作业照样触发,不会被当作「错过的运行」 而静默丢弃 —— 延后投递代表的是必须发生的动作。