Files
AgentLab/backend/app/db/models_triggers.py
T
ThearchyHelios 656a550408 数据事件触发按复核意见修正:途经驳回、去抖积压、source.changed 补写、按期次分窗口、补算缺省不认、示例出口字段同一套过滤
M1 途经驳回(编排者已定):outbox.stage_run_finished 收可选的 rejected_via(缺省 None 即没途经驳回),事件内容带 rejected 和
rejected_via,说法接「(途经驳回)」,模板有 {{ event.rejected }}(true / false)。触发条件缺省只认没途经驳回的,trigger 加
include_rejected(界面「也认途经驳回的」勾选,说明照实写)。runner._finalize 把 _rejected_via(state) 传进来那一下留给编排者
在合并时接(runner 的 _rejected_via 在 W1-b 的分支上);测试直接调 stage_run_finished 传参覆盖。

C1 去抖积压:以前一拍只走一个窗口,积压时后面的越等越久、全过了宽限,而且每个窗口各记一条跳过,原因还说调度没在跑。
现在 _drain 按发生时刻把等着的事件切成窗口,一拍里把到点的处理完(最多 100 个窗口、5000 条);排在前面已过补发宽限的合成
一条 skipped_misfire(合并了几次事件、几个窗口、时间跨度,上传表格另写合掉的期次),派给任务时已经晚于宽限才说「调度未运行」,
否则照实说积压;宽限内的照常发起(照重叠规则跳过的照实记)。

C3 source.changed:这一处确实没法和变化记录同一个事务(一次探查、剖析的变化分好几批落库,一批只触发一次要等任务结束才知道),
改成说实话:models_triggers 的说明写清哪两种同事务、哪一种不是。trigger_events 加 dedupe_key(source.changed:<任务号>,唯一),
emit_source_changed 按它去重;调度的一拍(刚起的服务第一拍就补,之后每 10 分钟)由 backfill_source_changes 按任务号补写:2 天内
测到、还开着的要处理级变化,任务已结束却没有事件的补一条。分组在 Python 里做,不用各库写法不同的 JSON 取值。

C4 按期次分窗口:triggers.window_key 让上传表格按期次各自成窗口,9 月、10 月连着启用各有各的触发;同一期的重传照旧合并。
两个窗口结束在同一刻时后一个挪 1 微秒,fire_at 不撞唯一约束。

C5 应用补算:workflow.finished 缺省不认 app_backfill 发起的(升版补算连发好几条、各是某一期的历史输入),trigger 加
include_backfill(界面「也认应用补算的」),选了「只认应用补算」时本来就认;说法写「途经驳回、应用补算的不算」。

C6 示例出口字段:triggers.outputs_of 一处过滤「_」开头的键,真派发、预览、立即运行共用;示例事件也按 matches 跳过缺省不认的
途经驳回、应用补算,取合条件的最近一次。

测试:新加 test_schedule_events_review.py(11 项,先写、先看着失败;去掉 matches 的驳回判断、把每拍窗口数改回 1 回跑,对应用例
各自失败),test_schedule_events_api.py 加一项「也认」两项经接口存取和说法;check-schedules 加「上游工作流的请求体」一段、纯函数
几条、触发历史里合并跳过那一行的夹具。
验证:后端全量 14479 通过;tsc、check-copy;check-schedules 447 项、check-runs 327 项通过(拿锁、--stacks 1)。
2026-10-18 18:09:20 +08:00

95 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""数据事件触发的待派发事件和去抖等待(docs/design/orchestration-round/00-总方案.md §4)。
由 app/db/models.py 末尾整模块导入,create_all 建表(都是新表,只加不改)。这里不导入 app.db.models:models.py 导入本模块时
自己还没加载完,外键一律按表名写。
**为什么要一张「待派发事件」表,而不是在发生处直接发起运行。** 事件发生在别的事务里:启用上传表格的新版本、正式运行的
收尾、记下数据源的变化。在那里直接发起运行,发起失败要不要回滚那次启用?重复发生、服务重启时发起到一半又算什么?说不清。
改成在发生处写一行 TriggerEvent,调度循环一拍取走、按各定时任务的触发条件派发:发起运行的规矩(重叠跳过、正式运行只从
发布版本发起、触发记录唯一)全部沿用定时任务那一套。
什么时候和那件事同一个事务:table.version(切指针那一下)、workflow.finished(写终态那一下)是同一个事务——发生了就一定有
这一行,没发生就一定没有。source.changed 不是:一次探查、剖析的变化分好几批落库,「这一批只触发一次」要等任务结束才知道,
事件在任务结束后另开会话写(data/source_changes.notify_after_job)。中间进程死了、库被锁住写不进,变化记录在、事件没有:
调度的一拍按任务号补写(scheduler/outbox.backfill_source_changes),dedupe_key(source.changed:<任务号>)唯一,补写和正常的
那一次不会记成两条。
调度关着(AGENTLAB_SCHEDULER)时没人取,事件留在表里,接口照实说有几条在等。第 2 波多进程时,取走靠的是条件 UPDATE,
多个进程也只有一个取得到。
- TriggerEvent 一次事件:种类(table.version / workflow.finished / source.changed)、对象 id、给模板用的事件内容、触发链
的深度和经过的运行、发生时刻、去重键(只有 source.changed 用)。status:pending 还没派发;routed 已经按触发条件派给了
matched 个定时任务(0 个也算派完)。
- TriggerDelivery 一个事件派给一个定时任务、等着去抖:同一个定时任务窗口内(第一条事件发生之后 debounce_s 秒)的几条
合成一次触发,fire_id 是那次触发记录;status:pending 等着、fired 合进了某次触发(发起、跳过、未能发起都算)、dropped
定时任务在等待期间被停用或改了触发条件,不再派发。(定时任务, 事件) 唯一:同一个事件不会派给同一个任务两次。
"""
from __future__ import annotations
from datetime import datetime
from typing import Any
from sqlalchemy import ForeignKey, Index, Integer, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, UTCDateTime, new_id, utcnow
#: 事件种类:上传表格启用了新的一期、工作流的正式运行成功结束、数据源出现要处理的变化
EVENT_KINDS = ("table.version", "workflow.finished", "source.changed")
#: 事件的状态:还没派发、已经派发(派给了 matched 个定时任务)
EVENT_STATUSES = ("pending", "routed")
#: 派给一个定时任务之后的状态:等着去抖、合进了一次触发、不再派发
DELIVERY_STATUSES = ("pending", "fired", "dropped")
class TriggerEvent(Base):
"""一次待派发的事件。payload 只放给输入模板用的值({{ event.* }},见 scheduler/triggers.py),workflow.finished 的
出口字段不复制进来:派发时按 run_id 读那次运行的成果(运行记录已经落库、不会再变)。"""
__tablename__ = "trigger_events"
__table_args__ = (
Index("ix_trigger_events_status", "status", "created_at"),
Index("ix_trigger_events_subject", "event", "subject_id"),
)
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
event: Mapped[str] = mapped_column(String(30))
#: 对象 id:表格数据源 id(table.version)、工作流 id(workflow.finished)、数据源 id(source.changed)
subject_id: Mapped[str] = mapped_column(String(32), default="")
#: 对象当时的名字(给人看的说法里用;对象之后改名、删除都不影响这一条)
subject_name: Mapped[str] = mapped_column(String(200), default="")
payload: Mapped[dict[str, Any]] = mapped_column(default=dict)
#: 触发链的深度:workflow.finished 时是上游那次运行自己由事件触发了几层(不是事件触发的为 0),别的种类为 0。
#: 派发时新运行的深度是它加 1,超过上限(scheduler/triggers.MAX_DEPTH)不发起
depth: Mapped[int] = mapped_column(Integer, default=0)
#: 经过的运行 id(从最上游起),给记录、排查循环用;只截最近的几条
chain: Mapped[list[Any]] = mapped_column(default=list)
occurred_at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)
status: Mapped[str] = mapped_column(String(20), default="pending")
routed_at: Mapped[datetime | None] = mapped_column(UTCDateTime, default=None)
#: 派给了几个定时任务
matched: Mapped[int] = mapped_column(Integer, default=0)
#: 去重键:同一件事只记一条。source.changed 是 source.changed:<任务号>(任务结束时记、没记上的补写,两边撞上唯一约束);
#: 别的种类为空(它们和那件事同一个事务,不会记两遍)
dedupe_key: Mapped[str | None] = mapped_column(String(80), default=None, unique=True)
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)
class TriggerDelivery(Base):
"""一个事件派给了一个定时任务,等着去抖、合成一次触发。"""
__tablename__ = "trigger_deliveries"
__table_args__ = (
UniqueConstraint("schedule_id", "event_id"),
Index("ix_trigger_deliveries_waiting", "schedule_id", "status", "occurred_at"),
)
id: Mapped[str] = mapped_column(String(32), primary_key=True, default=new_id)
schedule_id: Mapped[str] = mapped_column(ForeignKey("schedules.id", ondelete="CASCADE"))
event_id: Mapped[str] = mapped_column(ForeignKey("trigger_events.id", ondelete="CASCADE"))
#: 同事件的发生时刻(去抖窗口按它算)
occurred_at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)
status: Mapped[str] = mapped_column(String(20), default="pending")
#: 合进的那次触发(schedule_fires.id)
fire_id: Mapped[str | None] = mapped_column(String(32), default=None)
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=utcnow)