dijiekstra opened a new issue, #18627:
URL: https://github.com/apache/dolphinscheduler/issues/18627

   # DolphinScheduler Lakehouse Asset Snapshot Event-Driven Scheduling Design 
Document
   
   > Status: Draft / RFC
   > Author: Dolphin Agent
   > Related modules: dolphinscheduler-dao, dolphinscheduler-service, 
dolphinscheduler-master, dolphinscheduler-api, dolphinscheduler-task-plugin
   
   ## 1. Background and Goals
   
   ### 1.1 Background
   
   Modern CDC Lakehouse architectures (based on table formats such as Paimon / 
Iceberg / Hudi) are driving the evolution of data warehouse modeling and data 
platform scheduling paradigms:
   
   - Data warehouse modeling is evolving from a single “table” into a 
three-layer semantic model of “entity state table (latest-state) + event fact 
table (event fact) + change history table (changelog),” relying on capabilities 
provided by table formats such as Snapshot, Manifest, Primary Key Table, 
Changelog, Time Travel/Tag, and Schema Evolution.
   - The scheduling paradigm is evolving from a “time-based DAG” to “data asset 
state scheduling”: in other words, scheduling is no longer triggered only by 
cron time points, but by whether a data asset has advanced to a usable version. 
Typical scenarios include:
     - Trigger downstream processing after a snapshot is generated
     - Trigger windowed computation output after a watermark arrives
     - Allow metric publication only after a quality check passes
     - Trigger financial statement posting after a tag is solidified
     - Trigger downstream model upgrades after schema change approval passes
     - Trigger recomputation and reconciliation after backfill completes
   
   ### 1.2 Goals
   
   Without breaking DolphinScheduler’s existing scheduling semantics (cron 
scheduling, manual runs, backfill, failure recovery, DEPENDENT tasks, and 
normal DAG execution), add “data asset version advancement event-driven 
scheduling” capabilities to achieve:
   
   ```text
   湖仓表 snapshot/instant 推进
       -> 事件被感知并持久化为 AssetEvent
       -> 更新资产的当前状态 AssetState(latest snapshot / watermark / quality / schema)
       -> 按工作流/任务声明的资产依赖条件进行匹配评估
       -> 依赖满足后,通过 Command 机制触发/释放 DolphinScheduler 工作流或任务实例
       -> 执行、观测、失败重试与补偿
   ```
   
   This design follows the following non-negotiable principles:
   
   1. It must not be “trigger when the event arrives”; it must be “trigger when 
the asset state reaches the condition.”
   2. The same version / same dependency combination can only trigger once 
(idempotency).
   3. Events may be duplicated, out of order, delayed, or lost. Asset state is 
the basis for scheduling; events are only input evidence.
   4. Existing infrastructure such as Command / CommandType / DEPENDENT tasks / 
task instance state machines should be reused as much as possible. Do not build 
an independent scheduling kernel.
   5. In phase one, only implement a small, verifiable closed loop (Paimon 
snapshot -> AssetEvent -> AssetState -> dependency matching -> Command 
trigger), then extend to Iceberg/Hudi and multi-asset AND/OR combinations later.
   
   ### 1.3 Applicability Boundaries and Problem Definition
   
   This design targets **next-generation data warehouse architectures** (based 
on Lakehouse table formats such as Paimon/Iceberg/Hudi), whose core 
characteristics are:
   
   - **The entire DAG chain is driven by snapshot versions**: from ODS (data 
ingestion into the Lakehouse) to DWD (dimensional processing) to DWS 
(subject-area aggregation) to ADS (serving/output), each layer is the result of 
data asset version advancement in the previous layer, eliminating the mismatch 
between time and data availability.
   - **No reliance on cron or time expressions**: the trigger for any task in 
the DAG is based on its dependent assets (snapshot/watermark/quality/tag), not 
on “a fixed point in time.”
   - **A downstream task is triggered only when multiple upstream assets are 
ready together**: for example, a task in DWS may depend on snapshots of 
multiple DWD tables having advanced, and these dependencies are expressed 
through snapshot version combinations rather than time windows.
   - **Keep orchestration capabilities such as cross-flow dependencies**: if a 
task in the DAG declares cross-flow dependencies (depending on tasks in other 
workflows/projects), those dependencies should also be upgraded to a 
snapshot-based model to preserve the data-driven nature of overall scheduling.
   
   **Applicable scenarios**: any end-to-end data processing workflow in which 
ODS/DWD/DWS/ADS is carried by Lakehouse tables.
   
   **Not applicable**: traditional data warehouses still using “batch 
processing at fixed times” (even if already migrated to the cloud).
   
   ## 2. Current-State Analysis: Existing Event/Dependency Trigger 
Infrastructure in DolphinScheduler
   
   Based on an actual inspection of the codebase (version: current master 
branch), the following reusable capabilities already exist:
   
   ### 2.1 CommandType / Command (`dolphinscheduler-dao`, 
`dolphinscheduler-common`)
   
   - `org.apache.dolphinscheduler.common.enums.CommandType` (file: 
`dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/enums/CommandType.java`)
 already defines multiple trigger source types such as `START_PROCESS`, 
`SCHEDULER`, `COMPLEMENT_DATA`, and `RECOVER_TOLERANCE_FAULT_PROCESS` (the 
comment already contains `// todo: rename to WorkflowTriggerType`, indicating 
that Command is already treated internally as an abstraction of a “trigger 
type,” not just a “start action”).
   - `org.apache.dolphinscheduler.dao.entity.Command` 
(`dolphinscheduler-dao/.../entity/Command.java`) is the persisted entity mapped 
to table `t_ds_command`, with key fields including `commandType`, 
`workflowDefinitionCode`, `workflowDefinitionVersion`, `commandParam` 
(JSON-formatted startup parameters), and `workflowInstancePriority`.
   - `org.apache.dolphinscheduler.service.command.CommandService` 
(`dolphinscheduler-service/.../command/CommandService.java`) exposes `int 
createCommand(Command command)`, which is currently the unified entry point for 
“initiating one workflow instance run.” On the Master side, the 
`CommandService`/scanner consumes the `t_ds_command` table to create 
WorkflowInstance records.
   
   **Conclusion**: we do not need to build a parallel execution path for 
“event-triggered workflows.” Add a new `CommandType.ASSET_EVENT_TRIGGER` (or 
reuse `START_PROCESS` while marking the trigger source in `commandParam`), and 
write into `t_ds_command` through the existing `CommandService#createCommand`. 
This fully reuses the existing Master-side Command consumption, 
WorkflowInstance creation, and DAG execution chain. This is the lowest-risk 
integration point.
   
   ### 2.2 DEPENDENT Task (task-level dependency waiting)
   
   - `DependentType`, `DependentRelation` 
(`dolphinscheduler-task-plugin/dolphinscheduler-task-api/.../enums/DependentType.java`,
 `DependentRelation.java`): the current DEPENDENT task only supports 
dependencies on “whether another workflow/task succeeded within a certain 
cycle” (`DependentItem` contains `projectCode` / `definitionCode` / 
`depTaskCode` / `cycle` / `dateValue` / `dependResult`), and it is **oriented 
toward task instance execution results, not external data asset state**.
   - `DependentParameters` (same directory, 
`parameters/DependentParameters.java`) supports `DependentRelation` (AND/OR) 
for combining multiple `DependentTaskModel` entries. This structure for 
“evaluating multiple dependency items with AND/OR combinations” can be directly 
borrowed for expressing combinations of asset dependencies.
   - `DependentLogicTask` / `DependentTaskTracker` 
(`dolphinscheduler-master/.../executor/plugin/dependent/`): after a task 
instance enters the execution state, `DependentTaskTracker` polls for 
dependency completion, and `getDependentTaskStatus()` returns 
`TaskExecutionStatus`. In essence, this means “a task instance occupies an 
execution slot and internally performs polling waits,” rather than using event 
callback-driven execution.
   
   **Conclusion**: the “multiple dependency AND/OR combination expression” 
design from DEPENDENT tasks is worth reusing (specifically 
`DependentRelation`), but its “dependency object” is task execution result 
rather than external data asset version. Therefore, its `DependentItem` / 
`DependentTaskTracker` implementation cannot be reused directly. A new 
asset-oriented dependency model and evaluator must be added. Its execution form 
can, however, be used as a reference for a new task type `ASSET_SENSOR` 
(Strategy A, described later), reusing its “occupy task instance slot + poll 
for condition + continue downstream after success” running mode and state 
machine (`onTaskRunning` / `onTaskPaused` / `onTaskKilled`, etc., whose 
lifecycle hooks already exist in `AbstractLogicTask`).
   
   ### 2.3 Missing Capabilities (need to be added)
   
   - There is no first-class “Asset” model in the style of Airflow Datasets; 
there is no table such as `t_ds_asset`.
   - There is no unified ingestion layer for external events (messages / 
callbacks / polling); the current `dolphinscheduler-extract` module mainly 
contains internal RPC contracts between Master/Worker/API (`extract-master`, 
`extract-worker`, `extract-alert`, etc.), not event ingestion interfaces for 
external Lakehouse systems.
   - There is no “event deduplication + idempotent trigger history” table. The 
current `t_ds_command` table has no unique constraint preventing duplicate 
Command insertion for the same (workflow, trigger condition) pair. Existing 
business-level controls are implemented through scheduler/manual trigger points 
and do not cover “externally event-driven” scenarios.
   - There are no fields or state machines representing Lakehouse semantics 
such as watermark / snapshot / quality / schema.
   
   In summary, the **overall strategy** is to add three logical 
modules—`event-source`, `asset-state`, and `dependency-resolver`—plus the 
corresponding DAO tables. On the trigger side, integrate with the existing 
`CommandService.createCommand` (Strategy C, directly creating workflow 
instances) and add a new `ASSET_SENSOR` task type (Strategy A, task-level 
waiting that reuses embedded DAG waiting semantics). These two strategies 
should be implemented in phases, without invasive modifications to the Master 
scheduling kernel or task state machine.
   
   ## 3. Overall Architecture Design
   
   ### 3.1 End-to-End Flow
   
   ```text
   ┌───────────────┐   ┌────────────────┐   ┌───────────────┐   
┌─────────────────────┐   ┌───────────────┐
   │ Lakehouse      │   │ Event Ingestion│   │ Asset State   │   │ Dependency 
Resolver  │   │ Scheduler      │
   │ (Paimon/Iceberg│──▶│ (Poll Scanner /│──▶│ Store         │──▶│ (Match 
AssetDependency│──▶│ Adapter        │
   │  /Hudi commit) │   │  Report API)   │   │ (t_ds_asset_  │   │  -> 
READY/BLOCKED)   │   │ (CommandService│
   └───────────────┘   └────────────────┘   │  state)       │   
└─────────────────────┘   │  / ASSET_SENSOR│
                                             └───────────────┘                  
            │  Task)         │
                                                                                
             └───────┬────────┘
                                                                                
                     ▼
                                                                                
        Master 正常 Command/DAG 执行链路
   ```
   
   ### 3.2 Domain Model of Asset and Asset Event
   
   ```java
   // 资产唯一标识:catalog.database.table[/partition]
   public class AssetIdentifier {
       private String assetKey;      // 规范化后的唯一键,例如 
paimon://catalog/db/table/dt=2024-05-20
       private String storageFormat; // PAIMON / ICEBERG / HUDI
       private String catalogName;
       private String databaseName;
       private String tableName;
       private String partitionExpr; // 可为空,支持分区级资产
   }
   
   // 原始事件,来源可为轮询扫描或外部主动上报,仅作为“输入证据”,不可直接触发调度
   public class AssetEvent {
       private String eventId;         // 幂等键之一,来源系统内可重复计算得到(见 4.2)
       private String assetKey;
       private String eventType;       // SNAPSHOT_COMMITTED / 
WATERMARK_ADVANCED / TAG_CREATED /
                                        // QUALITY_CHECK_PASSED / 
QUALITY_CHECK_FAILED /
                                        // SCHEMA_CHANGE_APPROVED / 
BACKFILL_COMPLETED / CDC_LAG_RECOVERED
       private Long snapshotId;        // Paimon snapshot-id / Iceberg 
snapshot-id / Hudi instant time 映射值
       private Long schemaId;
       private Long watermark;         // epoch millis
       private String commitKind;      // APPEND / COMPACT / OVERWRITE / ...
       private Long commitTime;
       private Long deltaRecordCount;
       private Boolean schemaChanged;
       private String payload;         // 原始 JSON,便于排障,不参与判定逻辑
       private Long receiveTime;
   }
   
   // 资产当前状态:调度判定的唯一事实来源(Source of Truth)
   public class AssetState {
       private String assetKey;
       private Long latestSnapshotId;
       private Long latestSchemaId;
       private Long latestWatermark;
       private String latestTag;
       private String qualityStatus;   // UNKNOWN / PASSED / FAILED
       private String schemaStatus;    // UNKNOWN / COMPATIBLE / 
PENDING_APPROVAL / REJECTED
       private String backfillStatus;  // NONE / RUNNING / COMPLETED
       private Long updateTime;
       private Long version;           // 乐观锁,防止并发覆盖(见 4.4)
   }
   ```
   
   ### 3.3 Event Ingestion Design
   
   The two methods should coexist in parallel and compensate for each other. In 
the MVP phase, prioritize method one:
   
   **Method 1: Incremental Polling Scanner (Polling Scanner, priority 
implementation)**
   
   - Independently deploy a lightweight `AssetEventScanner` (either as a 
background task inside DolphinScheduler or as an independent process / 
scheduled API task) to periodically execute the following against registered 
Paimon tables:
     ```sql
     SELECT snapshot_id, schema_id, commit_user, commit_identifier, 
commit_kind, commit_time,
            watermark, total_record_count, delta_record_count, 
changelog_record_count
     FROM catalog_name.database_name.`table_name$snapshots`
     WHERE snapshot_id > ?
     ORDER BY snapshot_id;
     ```
     Here `?` comes from `last_scanned_snapshot_id` recorded in 
`t_ds_asset_event_consumer_offset`.
   - Advantages: no need to modify the writers; existing Paimon/Iceberg tables 
can be connected directly; it naturally provides “compensation” because it is 
inherently a pull model that can replay the full history.
   - Disadvantages: there is delay introduced by the scan interval (typically 
on the order of 10 seconds to 1 minute).
   
   **Method 2: Active Reporting by Writers (Push, for low-latency scenarios, 
introduced in a later phase)**
   
   - After a successful commit, Flink/Spark writing jobs call a new REST 
endpoint `POST /dolphinscheduler/asset-events` to actively report events (see 
Section 4.6 API design).
   - Advantages: low latency (second-level).
   - Disadvantages: requires changes to application code on the writer side; 
and it must coexist with polling scanner as a fallback, otherwise missed 
reports cannot be detected.
   
   **Conclusion (trade-off)**: in the MVP phase, implement only Method 1 
(Polling Scanner), because it is zero-intrusion to existing Flink/Paimon 
production pipelines, independently verifiable, and naturally compensating. 
Method 2 is a phase-two enhancement. Both methods share the same processing 
chain—“event deduplication -> state update -> dependency evaluation”—with only 
the source type (`sourceType=POLL` / `sourceType=PUSH`) differing. 
Deduplication is handled naturally by the unique constraint on 
`t_ds_asset_event`, so the two paths will not cause duplicate triggers.
   
   ### 3.4 Database Table Design (DDL draft)
   
   Add 5 new tables, all using the `t_ds_` prefix to align with existing naming 
conventions, and place them under new/upgrade SQL directories in 
`dolphinscheduler-dao` (following the organization of existing upgrade scripts 
such as 
`dolphinscheduler-dao/src/main/resources/sql/upgrade/<version>_schema/{mysql,postgresql}/dolphinscheduler_ddl.sql`).
   
   ```sql
   -- 资产注册表:描述一个可被依赖的湖仓数据资产
   CREATE TABLE t_ds_asset (
       id              BIGINT PRIMARY KEY AUTO_INCREMENT,
       asset_key       VARCHAR(512) NOT NULL,   -- 
catalog.db.table[/partition_expr] 规范化后的唯一键
       storage_format  VARCHAR(32)  NOT NULL,   -- PAIMON / ICEBERG / HUDI
       catalog_name    VARCHAR(128) NOT NULL,
       database_name   VARCHAR(128) NOT NULL,
       table_name      VARCHAR(128) NOT NULL,
       partition_expr  VARCHAR(256),
       description     VARCHAR(512),
       create_time     DATETIME NOT NULL,
       update_time     DATETIME NOT NULL,
       UNIQUE KEY uk_asset_key (asset_key)
   );
   
   -- 原始事件表:仅作为证据留存与排障,不作为调度判定依据
   CREATE TABLE t_ds_asset_event (
       id                     BIGINT PRIMARY KEY AUTO_INCREMENT,
       event_id               VARCHAR(128) NOT NULL,
       source_type             VARCHAR(16)  NOT NULL,  -- POLL / PUSH
       asset_key               VARCHAR(512) NOT NULL,
       event_type              VARCHAR(32)  NOT NULL,  -- SNAPSHOT_COMMITTED / 
WATERMARK_ADVANCED / ...
       snapshot_id              BIGINT,
       schema_id                BIGINT,
       watermark                BIGINT,
       commit_kind              VARCHAR(32),
       commit_time              BIGINT,
       delta_record_count       BIGINT,
       schema_changed           TINYINT,
       payload                  TEXT,
       receive_time             BIGINT NOT NULL,
       create_time              DATETIME NOT NULL,
       UNIQUE KEY uk_dedup (source_type, asset_key, event_type, snapshot_id),
       KEY idx_asset_snapshot (asset_key, snapshot_id),
       KEY idx_event_type_time (event_type, receive_time)
   );
   
   -- 资产状态表:调度判定的唯一事实来源
   CREATE TABLE t_ds_asset_state (
       id                 BIGINT PRIMARY KEY AUTO_INCREMENT,
       asset_key          VARCHAR(512) NOT NULL,
       latest_snapshot_id BIGINT,
       latest_schema_id   BIGINT,
       latest_watermark   BIGINT,
       latest_tag         VARCHAR(128),
       quality_status     VARCHAR(16) DEFAULT 'UNKNOWN',
       schema_status      VARCHAR(32) DEFAULT 'UNKNOWN',
       backfill_status    VARCHAR(16) DEFAULT 'NONE',
       version            BIGINT NOT NULL DEFAULT 0,   -- 乐观锁
       update_time        DATETIME NOT NULL,
       UNIQUE KEY uk_asset_key (asset_key)
   );
   
   -- 工作流/任务对资产的依赖声明
   CREATE TABLE t_ds_asset_dependency (
       id                       BIGINT PRIMARY KEY AUTO_INCREMENT,
       workflow_definition_code BIGINT NOT NULL,
       task_definition_code     BIGINT,             -- 为空表示对整个工作流生效(Strategy C)
       asset_key                VARCHAR(512) NOT NULL,
       dependency_group         VARCHAR(64) NOT NULL DEFAULT 'default', -- 
支持多资产 AND 分组
       condition_json           TEXT NOT NULL,       -- 见 3.6 依赖表达式
       enabled                  TINYINT NOT NULL DEFAULT 1,
       create_time              DATETIME NOT NULL,
       update_time              DATETIME NOT NULL,
       KEY idx_asset_key (asset_key),
       KEY idx_workflow (workflow_definition_code)
   );
   
   -- 幂等触发历史 + 审计
   CREATE TABLE t_ds_asset_trigger_history (
       id                        BIGINT PRIMARY KEY AUTO_INCREMENT,
       trigger_key               VARCHAR(256) NOT NULL,  -- 幂等键,见 4.1
       event_id                   VARCHAR(128),
       asset_key                  VARCHAR(512) NOT NULL,
       workflow_definition_code   BIGINT NOT NULL,
       workflow_instance_id       BIGINT,
       task_definition_code       BIGINT,
       task_instance_id           BIGINT,
       trigger_status             VARCHAR(16) NOT NULL, -- TRIGGERING / SUCCESS 
/ FAILED / SKIPPED
       reason                     VARCHAR(512),
       create_time                DATETIME NOT NULL,
       update_time                DATETIME NOT NULL,
       UNIQUE KEY uk_trigger_key (trigger_key),
       KEY idx_status_time (trigger_status, create_time)
   );
   
   -- 事件消费位点(用于 Polling Scanner 的断点续扫)
   CREATE TABLE t_ds_asset_event_consumer_offset (
       id                    BIGINT PRIMARY KEY AUTO_INCREMENT,
       source_type            VARCHAR(16) NOT NULL,
       asset_key              VARCHAR(512) NOT NULL,
       consumer_group         VARCHAR(64) NOT NULL DEFAULT 'default',
       last_snapshot_id       BIGINT,
       last_scan_time         DATETIME,
       UNIQUE KEY uk_offset (source_type, asset_key, consumer_group)
   );
   ```
   
   ### 3.5 Scheduling Trigger Flow (integrated with the existing Command 
mechanism)
   
   ```text
   1. AssetEventScanner 定时扫描 Paimon $snapshots -> 产出候选 AssetEvent
   2. 写入 t_ds_asset_event(利用唯一约束 uk_dedup 做插入去重,冲突则说明该事件已处理过,直接跳过)
   3. 事件写入成功后,进入 AssetState 更新:
      - 读取当前 t_ds_asset_state(带 version)
      - 若 event.snapshotId <= state.latestSnapshotId,丢弃(乱序保护,见 4.3)
      - 否则以乐观锁 UPDATE ... WHERE asset_key=? AND version=? 更新状态,version+1
   4. 状态更新成功后,触发 DependencyResolver:
      - 查询该 asset_key 关联的 t_ds_asset_dependency
      - 对每条依赖,按 condition_json 
求值(快照存在/watermark>=X/quality=PASSED/schema=COMPATIBLE)
      - 对同一 dependency_group 下的多条依赖做 AND 聚合(同一工作流等待多个上游资产都就绪)
   5. 若判定为 READY:
      a. 计算 trigger_key(见 4.1),向 t_ds_asset_trigger_history 做唯一插入
         - 插入成功 => 本次获得触发权,继续步骤 b
         - 插入失败(唯一冲突)=> 说明已被其他并发的 Master/Scanner 实例触发过,直接返回,不重复触发
      b. 根据依赖类型选择触发方式:
         - Strategy C(工作流级):调用现有 CommandService#createCommand,
           使用新增 CommandType(如 ASSET_EVENT_TRIGGER,或复用 START_PROCESS 并在 
commandParam 中标记
           triggerSource=ASSET_EVENT、assetKey、snapshotId 等审计信息),
           由 Master 侧既有 Command 消费链路完成 WorkflowInstance 创建与 DAG 执行,不改动 Master 
内核。
         - Strategy A(任务级):若工作流已在运行且包含 ASSET_SENSOR 任务,
           则更新对应任务实例的依赖满足标记,由 ASSET_SENSOR 任务(运行时轮询/订阅 AssetState 变化)
           自行判定成功,走入现有任务状态机(复用 DependentLogicTask 同款生命周期钩子)。
      c. 在真正触发前执行"原生依赖门禁校验":
         - 若用户配置了必须先满足的原生依赖(上游任务成功、DEPENDENT 条件、运行窗口限制),则先校验通过再触发;
         - 仅在显式配置为 snapshot 直驱模式时,可由资产版本推进直接作为主触发条件。
      d. 触发结果(成功/失败)回写 t_ds_asset_trigger_history.trigger_status,
         失败的记录由 Compensation Scanner 定期扫描重试。
   6. Master 按照现有正常链路执行 WorkflowInstance / TaskInstance,无需感知事件来源。
   ```
   
   ### 3.6 Dependency Expression (structured configuration, not a general DSL)
   
   ```json
   {
     "assetKey": "paimon://catalog/db/ods_order",
     "snapshotRequired": true,
     "qualityStatus": "PASSED",
     "schemaStatus": "COMPATIBLE"
   }
   ```
   
   `dependency_group` is used to express multi-asset AND semantics: all 
dependencies within the same group must evaluate to READY before that group is 
considered ready; a workflow can configure multiple groups, and groups are 
OR-related (any one group being ready is sufficient to trigger). The semantics 
remain aligned with `DependentParameters.Dependence.relation` (AND/OR), making 
it easier for users to understand and migrate. In the initial phase, support 
only the two-level structure of AND-within-group / OR-across-group, without 
supporting general nested Boolean expressions.
   
   ### 3.7 Idempotency Design
   
   `trigger_key` composition (see 4.1) together with the unique constraint on 
`t_ds_asset_trigger_history` is the core guarantee for idempotency:
   
   - The same combination of workflowDefinitionCode + dependency_group + 
snapshot set for all related assets can trigger successfully only once.
   - When multiple Masters/Scanners concurrently scan the same batch of events, 
the database unique constraint naturally ensures that “only one winner” exists, 
with no need for a distributed lock.
   - If `createCommand` fails (network/DB jitter), `trigger_status` remains 
`TRIGGERING`. The Compensation Scanner identifies records stuck without a 
terminal state and safely retries them (before retrying, it must confirm 
whether the corresponding Command/WorkflowInstance has already been created, to 
avoid duplicate creation—for example by writing back `trigger_key` into 
`commandParam` and checking for an associated Command/WorkflowInstance before 
retrying).
   
   ### 3.8 Multi-Asset Alignment (AND/OR)
   
   - Dependencies are stored in `t_ds_asset_dependency`, grouped by 
`dependency_group`. Each evaluation-trigger attempt must read the entire group, 
evaluate `condition_json` item by item, and enter the trigger flow only if all 
are satisfied.
   - To avoid the issue of “asset A becomes ready -> trigger evaluation runs -> 
asset B is not ready -> give up” and then never re-triggers after B becomes 
ready, **the arrival of any asset event re-evaluates all dependency groups 
associated with that asset** (not only the asset itself). In other words, 
DependencyResolver uses the reverse index `asset_key -> dependency_group` to 
drive re-evaluation.
   
   ### 3.9 Out-of-Order Event Handling
   
   - At the event layer: the unique constraint on `t_ds_asset_event` is defined 
by `(source_type, asset_key, event_type, snapshot_id)`, so duplicate reports 
are rejected directly by the database and are considered successfully 
deduplicated.
   - At the state layer: when updating `AssetState`, version numbers 
(`snapshotId` / `watermark`) are compared strictly, and the strategy is 
“advance only, never roll back”:
     ```sql
     UPDATE t_ds_asset_state
     SET latest_snapshot_id = ?, latest_watermark = GREATEST(latest_watermark, 
?),
         version = version + 1, update_time = ?
     WHERE asset_key = ? AND version = ? AND ? > latest_snapshot_id;
     ```
     If the affected row count of `UPDATE` is 0, it indicates either a 
concurrent conflict or an expired event (an old snapshot arrived late). The 
event is marked `IGNORED` and the reason is recorded, without affecting 
scheduling decisions.
   
   ### 3.10 Failure Retry and Compensation
   
   - **Compensation Scanner** (reusing the Polling Scanner infrastructure from 
3.3, with an independent schedule such as once every 5 to 15 minutes) is 
responsible for:
     1. Comparing the actual latest Paimon snapshot with 
`t_ds_asset_state.latest_snapshot_id`. If it is behind, a collection gap exists 
(Push failed or the Scanner was temporarily down), so the missing snapshots are 
re-fetched and backfilled into `t_ds_asset_event`.
     2. Scanning `t_ds_asset_trigger_history` for records that remain in 
`TRIGGERING` / `FAILED` state for a long time, and re-executing the trigger 
(first checking whether the corresponding Command/WorkflowInstance already 
exists to avoid duplicate triggering).
     3. For dependencies in `BLOCKED_BY_*` status, providing a query interface 
explaining “why it has not triggered yet” (see observability).
   - This compensation path shares exactly the same deduplication and state 
update path as Push events, ensuring that the two paths do not cause duplicate 
triggers.
   
   ## 4. Integration Points with Existing Modules
   
   ### 4.1 How workflow/task definitions declare asset dependencies
   
   Two implementation forms are recommended, and should be delivered 
incrementally by phase:
   
   **(1) Strategy A: add a new task type `ASSET_SENSOR` (recommended MVP first 
choice, lowest risk)**
   
   - Following the plugin mechanism of the existing `DEPENDENT` task 
(`DependentLogicTaskChannel` / `DependentLogicTaskChannelFactory` / 
`DependentLogicTask`), add a new module `dolphinscheduler-task-asset-sensor` 
under `dolphinscheduler-task-plugin`:
     - `AssetSensorParameters extends AbstractParameters`: the field structure 
follows the dependency expression JSON in 3.6, plus `checkIntervalSeconds` and 
`timeoutMinutes`.
     - `AssetSensorLogicTask extends AbstractLogicTask<AssetSensorParameters>`: 
reuse the lifecycle of `AbstractLogicTask` 
(`onTaskRunning/onTaskPaused/onTaskKilled`), and internally poll 
`AssetStateDao` to determine whether the condition is satisfied (or register a 
callback and let DependencyResolver actively push READY status to reduce 
polling latency).
     - Users add an `ASSET_SENSOR` node in the workflow DAG as an “entry 
sentinel task,” and downstream tasks are linked using normal DAG dependencies. 
After the task succeeds, the DAG proceeds normally.
   - By default, it is recommended to place `ASSET_SENSOR` before existing 
business tasks as a data-readiness gate, rather than replacing existing DAG 
dependencies. This allows both “data version readiness” and “user-configured 
native dependencies” to be satisfied together.
   - Advantages: fully reuse the existing WorkflowInstance/TaskInstance state 
machines, retries, timeouts, alerts, and UI display (through the task type 
registration mechanism); minimal change scope; effectively equivalent to 
“adding a new task plugin to the DAG.”
   
   **(2) Strategy C: workflow-level events directly create workflow instances 
(for pure event-driven scenarios, phase-two implementation)**
   
   - Add `assetTriggerEnabled` and the associated `dependency_group` to the 
extended properties of workflow definitions (records with empty 
`task_definition_code` in `t_ds_asset_dependency` can represent workflow-level 
dependencies).
   - After DependencyResolver determines READY, call 
`CommandService#createCommand`, passing audit fields related to triggering such 
as `assetKey` and `snapshotId` in `commandParam`.
   - Risk note: the mapping between workflow instances and snapshot versions 
must be explicitly defined (see Section 11 risks).
   
   ### 4.2 New API endpoints on the API layer
   
   Add a new Controller in `dolphinscheduler-api` (following the layering style 
of the existing `ExecutorController`: Controller -> Service -> DAO):
   
   ```text
   POST   /projects/{projectCode}/assets                       注册/更新资产
   GET    /projects/{projectCode}/assets                        查询资产列表
   GET    /projects/{projectCode}/assets/{assetKey}/state       查询资产当前状态
   POST   /projects/{projectCode}/asset-events                  外部系统主动上报事件(Push 
方式)
   GET    /projects/{projectCode}/asset-events                  查询事件历史(排障)
   POST   /projects/{projectCode}/asset-dependencies             为工作流/任务声明资产依赖
   GET    /projects/{projectCode}/asset-dependencies/{workflowDefinitionCode}  
查询依赖及其当前判定状态(为什么没触发)
   GET    /projects/{projectCode}/asset-trigger-history          查询触发历史(幂等审计)
   ```
   
   ### 4.3 UI layer suggestions (not the focus of this round, brief only)
   
   - In the workflow definition canvas, an `ASSET_SENSOR` task node should 
display the currently waited asset(s), condition expression(s), and current 
AssetState snapshot comparison (how many snapshots behind / how far watermark 
lags).
   - Add an “asset dashboard” page: asset list, recent events, current state, 
associated downstream workflows, and trigger history timeline, similar to 
Airflow’s Dataset view.
   
   ## 4.4 Asset dependency patterns within the DAG chain
   
   In next-generation data warehouse DAGs, the trigger for every task should 
follow a **pure snapshot model**, without time expressions or native dependency 
gates:
   
   | DAG Layer | Dependency Source | Example | Description |
   |---|---|---|---|
   | **ODS (data ingestion into the Lakehouse, first layer)** | Snapshot 
advancement from external data sources | Flink CDC -> Paimon ods_order_latest 
snapshot | The initial source of data asset versions, typically with snapshot 
events produced by external data engines (Flink/Spark) |
   | **DWD (dimensional processing, middle layer)** | Depends on snapshots from 
the previous ODS layer | Wait until snapshots of ods_order_latest and 
ods_payment_latest have both advanced | Multiple upstream assets are combined 
using AND conditions |
   | **DWS (subject-area aggregation, middle layer)** | Depends on multiple DWD 
snapshots | Wait until snapshots of dwd_trade_event and dwd_order_dim advance | 
Also a multi-asset AND dependency, aligned with DAG predecessor/successor 
dependencies |
   | **ADS (serving/output, terminal layer)** | Depends on DWS snapshots | Wait 
until snapshots of dws_gmv_daily and dws_order_stat advance | The final data 
product layer |
   
   **Key principles**:
   - The trigger for any task in the DAG is **entirely driven by the snapshot 
versions of its dependent assets**, without time expressions or cron.
   - “Predecessor/successor dependencies” in the DAG are automatically 
transformed into “snapshot dependencies”: the later task waits for the snapshot 
produced by the earlier task.
   - Cross-flow dependencies (depending on tasks in other DAGs) are also 
upgraded to a snapshot model by referencing the asset snapshots produced by 
external DAGs.
   - **There is no need to mix native dependencies and asset dependencies**: in 
next-generation data warehouse architectures, asset snapshots are the sole 
trigger criterion.
   
   ## 4.5 Impact on and Adaptations to the Existing Master Scheduling 
Architecture
   
   ### 4.5.1 Overview of the current Master scheduling flow
   
   The core scheduling flow of DolphinScheduler Master is as follows:
   
   ```text
   1. CommandService 定时扫描 t_ds_command 表(按 id 递增)
      ↓
   2. 对每条 Command 记录:
      a) 检查 commandType(START_PROCESS / SCHEDULER / COMPLEMENT_DATA / ...)
      b) 读取关联的 workflow_definition(工作流定义)
      c) 创建 WorkflowInstance(工作流实例,初始状态=SUBMITTED)
      d) 生成该工作流的 DAG 实例(TaskInstance + TaskDependency)
      ↓
   3. MasterScheduler 定时扫描 WorkflowInstance / TaskInstance(按状态分组)
      ↓
   4. 工作流实例状态转移:
      SUBMITTED -> RUNNING(任何非跳过的任务已提交)
                -> SUCCESS(所有任务成功)
                -> FAILURE(任何关键路径任务失败)
      ↓
   5. 任务实例状态转移(核心):
      SUBMITTED -> READY/WAITING_DEPENDENCY(依赖不满足)
                -> RUNNING(所有依赖满足,发送给 Worker)
                -> SUCCESS/FAILED/...
      ↓
   6. 依赖判定逻辑(当前):
      - DEPENDENT 任务:轮询查询上游任务实例状态(是否 SUCCESS)
      - DAG 前后置依赖:检查前置任务是否 SUCCESS
      - 其他:判定条件立即满足,直接 READY
      ↓
   7. Worker 执行 TaskInstance,汇报结果
      ↓
   8. 重复步骤 3-7,直到工作流完成
   ```
   
   ### 4.5.2 Integration points for asset-event scheduling
   
   After asset event-driven scheduling is introduced, Master needs to handle 
**two trigger sources** and **three layers of dependency evaluation**:
   
   **Trigger sources (Command creation)**:
   
   ```text
   Original:  START_PROCESS / SCHEDULER / COMPLEMENT_DATA / ...
              ↓
   Enhanced:  新增 ASSET_EVENT_TRIGGER / 或复用 START_PROCESS + 特殊 commandParam
              ↓
              由 DependencyResolver 模块判定资产就绪后调用 CommandService#createCommand
              写入 t_ds_command(与现有流程完全相同)
   ```
   
   **Three layers of dependency evaluation (from outer to inner)**:
   
   ```text
   第一层:工作流级依赖(Strategy C,二期实现)
     ├─ 资产依赖判定:DependencyResolver 评估 
t_ds_asset_dependency(task_definition_code=null 的记录)
     └─ 不满足 => 不创建 Command / 工作流实例
   
   第二层:任务级依赖(Strategy A,MVP)
     ├─ 原有 DAG 前后置依赖:Master 现有逻辑,检查前置任务是否 SUCCESS
     ├─ ASSET_SENSOR 任务依赖:轮询 t_ds_asset_state,判定条件(snapshot / watermark / 
quality)
     └─ DEPENDENT 任务依赖:轮询上游任务实例状态
   
   第三层:工作流内部 DAG 依赖
     └─ 任务间的串联 / 并联,正常的 DAG 拓扑执行
   ```
   
   ### 4.5.3 Specific Master-side adaptations
   
   #### Adaptation 1: integrate DependencyResolver into Master
   
   **Conceptual model**:
   
   ```java
   public interface DependencyResolver {
       /**
        * 评估一个 dependency_group 是否 READY
        * @param workflowCode 工作流定义编码(用于工作流级依赖评估)
        * @param taskCode 任务定义编码(如果为 null,表示工作流级依赖)
        * @param dependencyGroup 依赖分组名
        * @return 依赖是否 READY(true 可以触发 / 释放,false 继续等待)
        */
       DependencyStatus resolveDependency(Long workflowCode, Long taskCode, 
String dependencyGroup);
       
       /**
        * 注册资产状态变化的监听器
        * DependencyResolver 内部可基于事件回调而非轮询,降低延迟
        */
       void registerAssetStateChangeListener(AssetStateChangeListener listener);
   }
   ```
   
   **Timing for workflow-level triggering (Strategy C)**:
   
   ```text
   在 DependencyResolver 判定工作流级依赖 READY 后,立即调用:
     commandService.createCommand(
       Command.builder()
         .commandType(CommandType.ASSET_EVENT_TRIGGER)
         .workflowDefinitionCode(workflowCode)
         .commandParam(JSON 序列化的 {assetKey, snapshotId, triggerKey, ...})
         .build()
     );
   
   然后在 t_ds_asset_trigger_history 中 INSERT 占位记录(trigger_key 唯一约束)。
   
   Master 的现有 CommandService scanner 会照常消费这条 Command,创建 WorkflowInstance,执行 DAG。
   无需在 Master 侧做特殊识别——完全复用现有流程。
   ```
   
   #### Adaptation 2: execution lifecycle of the `ASSET_SENSOR` task type
   
   **Behavior during the Master execution phase**:
   
   ```text
   1. TaskInstance 状态 = SUBMITTED,判定是否可进入 READY
      ↓
   2. MasterScheduler 调用 task plugin 的 tracker(AssetSensorTracker extends 
TaskTracker)
      ↓
   3. AssetSensorTracker.checkTaskStatus() 周期性调用 
DependencyResolver#resolveDependency()
      返回值:
      - DependencyStatus.READY => 任务转为 READY,发送给 Worker 执行
      - DependencyStatus.BLOCKED => 保持 WAITING_DEPENDENCY,继续轮询
      - DependencyStatus.TIMEOUT => 任务转为 ERROR(超时失败)
      ↓
   4. Worker 侧照常执行该任务(可能是 Shell / SQL,或空操作代表"依赖已满足")
      ↓
   5. 下游任务通过 DAG 前后置依赖关系正常推进
   ```
   
   **Code integration points** (`dolphinscheduler-master` module):
   
   ```text
   
dolphinscheduler-master/src/main/java/org/apache/dolphinscheduler/server/master/
   ├── engine/
   │   └── DAGExecutionEngine.java  (现有,DAG 状态机)
   │       ├── 改造点1:在 commitTask / dispatchTask 前检查 ASSET_SENSOR 任务
   │       │   if (isAssetSensorTask) {
   │       │       // 调用 DependencyResolver 检查依赖
   │       │       if (!dependencyResolver.resolveDependency(...).isReady()) {
   │       │           return; // 不提交给 Worker,保持 WAITING_DEPENDENCY
   │       │       }
   │       │   }
   │       └── 改造点2:在工作流 SUBMITTED -> RUNNING 转移前
   │           if (isAssetEventTrigger && hasWorkflowLevelAssetDependency) {
   │               // 可选:在 Master 侧提前检查,尽早发现不满足的依赖
   │               // (否则直接创建实例,在工作流执行时逐个任务检查)
   │           }
   ├── processor/
   │   ├── WorkflowProcessor.java (工作流实例处理)
   │   │   └── 改造点3:处理 CommandType.ASSET_EVENT_TRIGGER
   │   │       解析 commandParam 提取 assetKey / snapshotId 等信息,
   │   │       存入 WorkflowInstance 的 commandParam 或扩展字段
   │   │
   │   └── TaskProcessor.java (任务实例处理)
   │       └── 改造点4:任务状态转移时,对 ASSET_SENSOR 任务特殊处理
   │           调用 AssetSensorTracker.checkTaskStatus()
   │
   └── service/
       ├── DependencyResolverService.java (新增)
       │   ├── 注入 AssetStateService (DAO 层)
       │   ├── 注入 AssetDependencyService (DAO 层)
       │   └── 实现 resolveDependency 逻辑
       │       a) 查询 t_ds_asset_dependency(task_definition_code=?, 
dependency_group=?)
       │       b) 逐一查询 t_ds_asset_state 对应的 asset_key
       │       c) 对每个资产条件求值(snapshot exists, watermark gte, quality status, ...)
       │       d) 组合结果(同 group 所有条件 AND,不同 group 间 OR)
       │
       └── TrackerManager.java (现有,但需扩展)
           └── 改造点5:注册 ASSET_SENSOR 对应的 tracker
               trackerFactory.get(TaskType.ASSET_SENSOR) 
               => new AssetSensorTracker(dependencyResolverService)
   ```
   
   #### Adaptation 3: workflow instance startup and handling of SUBMITTED status
   
   **Current logic** (simplified):
   
   ```java
   // MasterScheduler#submitWorkflow
   Command command = commandService.findOne(commandId);
   WorkflowInstance instance = new WorkflowInstance();
   instance.setWorkflowDefinitionCode(command.getWorkflowDefinitionCode());
   instance.setState(WorkflowExecutionStatus.SUBMITTED);
   // ... 生成 TaskInstance,立即进入 DAG 执行
   ```
   
   **New logic** (if workflow-level asset dependency checks are required):
   
   ```java
   // 在 WorkflowInstance 创建后、生成 DAG 任务前
   if (command.getCommandType() == CommandType.ASSET_EVENT_TRIGGER) {
       // 可选:提前验证工作流级资产依赖是否已满足
       // (这是一个检查点,如果仍未满足则可能是 Compensation 重试时的竞态)
       boolean workflowLevelAssetReady = dependencyResolverService
           .resolveDependency(workflowCode, null, "default")
           .isReady();
       
       if (!workflowLevelAssetReady) {
           // 记录告警,可选地延迟创建 TaskInstance
           log.warn("Workflow {} triggered by asset event but asset dependency 
no longer ready. " +
                    "This may indicate compensation retry or state backtrack.", 
workflowCode);
       }
   }
   
   // 正常流程继续,生成 DAG 任务
   for (TaskDefinition taskDef : dag.getTasks()) {
       TaskInstance taskInstance = new TaskInstance();
       taskInstance.setTaskDefinitionCode(taskDef.getCode());
       taskInstance.setState(TaskExecutionStatus.SUBMITTED);
       if (isAssetSensorTask(taskDef)) {
           // ASSET_SENSOR 任务状态初始化为 WAITING_DEPENDENCY
           taskInstance.setState(TaskExecutionStatus.WAITING_DEPENDENCY);
       }
       // ... 保存 taskInstance
   }
   ```
   
   #### Adaptation 4: Master’s loop scanning logic
   
   **Existing scan cycle**:
   
   ```text
   CommandService.scanner()  // 扫描 t_ds_command,周期=配置(默认 5s)
     ↓
   WorkflowProcessor.processWorkflow()  // 处理工作流状态转移
     ↓
   TaskProcessor.processTask()  // 处理任务状态转移,检查依赖
     ↓
   AbstractTaskTracker.checkTaskStatus()  // 调用具体 task plugin 的 tracker(如 
DependentTaskTracker)
   ```
   
   **New scan logic** (does not conflict with the existing one):
   
   ```text
   新增 AssetEventCompensationScanner(可选,作为 Master 内部的后台线程)
     周期:5~15 分钟(独立于 CommandService 扫描)
     ↓
     扫描 t_ds_asset_event 和 t_ds_asset_state,检查:
     1. 是否有漏采集的事件(e.g. Paimon 实际最新 snapshot_id > 
t_ds_asset_state.latest_snapshot_id)
     2. 是否有卡顿的 trigger_history(TRIGGERING 或 FAILED 状态超过 N 分钟)
     ↓
     对于漏采集:补写 t_ds_asset_event(去重自动处理)-> 触发 DependencyResolver 重新评估
     对于卡顿:可选地重试 createCommand(检查唯一约束冲突,确保不重复)
     ↓
     该扫描是"兜底补偿",不影响主调度流程
   ```
   
   ### 4.5.4 Minimal impact on the existing architecture
   
   **No changes required**:
   
   - ✅ The core state machines of WorkflowInstance and TaskInstance (SUBMITTED 
/ RUNNING / SUCCESS / FAILED / ...)
   - ✅ The expression and execution of DAG predecessor/successor dependencies 
(kept as-is)
   - ✅ Existing logic for DEPENDENT tasks (fully independent, no interference)
   - ✅ Worker task execution lifecycle (transparent / unaffected)
   - ✅ Peripheral capabilities such as Alert / Monitor / Backfill (compatible)
   
   **Changes required** (but all are plugin-based and non-invasive):
   
   - Add the new `ASSET_SENSOR` task type (as a task plugin similar to 
`DependentLogicTask`)
   - Add `AssetSensorTracker` as the tracker for that task (implementing the 
TaskTracker interface)
   - Add `DependencyResolverService` to handle dependency evaluation logic 
(Service layer, without changing the Master kernel)
   - Add `CommandType.ASSET_EVENT_TRIGGER` (optional, or reuse START_PROCESS)
   - Add a single check before `TaskProcessor#submitTask()`:
     ```java
     if (task is ASSET_SENSOR) {
         if (!dependencyResolver.resolveDependency(...).isReady()) {
             return; // 不提交
         }
     }
     ```
   
   **Estimated code changes**:
   
   Estimated **< 500 lines** (including the new `DependencyResolverService`, 
`AssetSensorTracker`, and the required integration checks), which is a very 
small change relative to DolphinScheduler’s overall codebase size.
   
   ### 4.5.5 Concurrency and consistency guarantees
   
   **Multi-Master concurrency scenario**:
   
   ```text
   场景:5 个 Master 实例同时运行,都在扫描 t_ds_command 和执行工作流
   
   冲突点1:CommandService.createCommand() 写入同一条 Command
     => 依赖 t_ds_command 的 PK 或 sequence,一般由应用侧写入,不会重复
   
   冲突点2:多个 Master 同时评估 ASSET_SENSOR 任务的依赖,决定是否提交给 Worker
     => 每个 Master 各自维护 TaskTracker,评估结果本地缓存
     => 当依赖首次从 NOT_READY 转为 READY 时,最快的 Master 抢到 Worker 资源
     => TaskProcessor#submitTask() 会检查 TaskInstance 是否已有 Worker 分配,重复提交会被幂等处理
     => 无需分布式锁,因为 TaskInstance 表会做状态检查
   
   冲突点3:多个 Master 同时插入 t_ds_asset_trigger_history
     => 唯一约束 uk_trigger_key 保证只有一个成功
     => 失败的 Master 捕获唯一冲突异常,记录日志后继续(不影响调度)
   ```
   
   **Idempotency guarantee**:
   
   ```text
   trigger_key 形如:
     workflow_12345 + dependency_group_default + snapshot_1001 + snapshot_1002 
+ ...
     = 完整标识"该工作流依赖这一组快照组合"的唯一键
   
   同一 trigger_key 最多被成功创建一次 t_ds_asset_trigger_history 记录。
   对应的 Command 也最多被创建一次(或可在 commandParam 中埋入 trigger_key 做检查)。
   因此,无论有多少个 Master 并发评估,最终只会创建一个 WorkflowInstance。
   ```
   
   ### 4.5.6 Key configuration items
   
   New Master-side configuration is required (to be written into Master’s 
`application.yaml`):
   
   ```yaml
   # 资产事件驱动调度开关
   dolphinscheduler:
     asset-event-scheduling:
       enabled: true                           # 全局开关
       
       # DependencyResolver 配置
       dependency-resolver:
         cache-expire-seconds: 60              # 资产状态本地缓存过期时间
         resolve-timeout-seconds: 5            # 单次依赖评估的超时时间
         
       # AssetEventCompensationScanner 配置
       compensation-scanner:
         enabled: true
         interval-seconds: 300                 # 补偿扫描周期(5分钟)
         batch-size: 100                       # 每次扫描的批量大小
         
       # 事件源配置
       event-source:
         paimon-polling:
           enabled: true
           interval-seconds: 30                # Paimon $snapshots 轮询周期
           batch-size: 500
         push-receiver:
           enabled: true                       # 是否启用 HTTP/Webhook 接收事件
   ```
   
   All of these configurations are **optional and incremental**. In the MVP 
phase, it is enough to enable only `enabled: true` and use the default values.
   
   ---
   
   ## 5. End-to-End Example: GMV Scenario Based on a Paimon Order Table
   
   ### 5.1 Business process and data assets
   
   ```text
   MySQL 订单库(order / payment / refund 表)
     -> Flink CDC 持续消费 binlog,生产 Paimon snapshot
     -> Paimon 湖仓表:
        ods_order_latest(主键表,最新订单状态)→ snapshot推进
        ods_payment_latest(主键表,最新支付状态)→ snapshot推进
     -> DWD 层任务:dwd_trade_event_fact(识别支付/退款事件)
        依赖 ods_order_latest + ods_payment_latest 快照都推进
     -> DWS 层任务:dws_gmv_daily(每日GMV聚合)
        依赖 dwd_trade_event_fact 快照推进
     -> ADS 层任务:ads_gmv_dashboard(对外报表/API)
        依赖 dws_gmv_daily 快照推进
   ```
   
   ### 5.2 Workflow definition (DAG structure) — pure snapshot model
   
   **Next-generation data warehouse DAG structure** (no cron, no time 
expressions):
   
   ```text
   Flink CDC 持续运行,生产 ods_order_latest、ods_payment_latest snapshot
     ↓
   DAG工作流启动(由外部系统触发,例如Airflow或DolphinScheduler的API)
     ↓
   Task 1: DWD_trade_event_fact
     依赖:ods_order_latest snapshot_id >= N1 AND ods_payment_latest snapshot_id 
>= N2
     执行:读两张表的最新快照,识别交易事件
     产出:dwd_trade_event_fact snapshot
     ↓
   Task 2: DWS_gmv_daily
     依赖:dwd_trade_event_fact snapshot_id >= M1
     执行:聚合每日GMV
     产出:dws_gmv_daily snapshot
     ↓
   Task 3: ADS_gmv_dashboard
     依赖:dws_gmv_daily snapshot_id >= P1
     执行:同步到报表库
     产出:ads_gmv_dashboard 就绪
   ```
   
   **Core improvements**:
   - Completely cron-free, with no fixed execution time. Once the DAG starts, 
execution of each task depends on advancement of its dependent snapshots.
   - If Flink CDC is delayed and ODS snapshots advance slowly, DWD waits; once 
the snapshots arrive, DWD executes immediately.
   - Snapshots from multiple upstream assets (`ods_order_latest + 
ods_payment_latest`) are combined using AND conditions.
   
   ### 5.3 Example asset dependency declarations
   
   **Dependencies of the DWD layer on two ODS tables**:
   
   ```json
   [
     {
       "workflowDefinitionCode": 12345,
       "taskDefinitionCode": 100,    // DWD_trade_event_fact 任务
       "assetKey": "paimon://ods_db/ods_order_latest",
       "dependencyGroup": "trade_event_group",
       "conditionJson": {
         "assetKey": "paimon://ods_db/ods_order_latest",
         "snapshotRequired": true,
         "qualityStatus": "PASSED",
         "schemaStatus": "COMPATIBLE"
       },
       "enabledFlag": 1
     },
     {
       "workflowDefinitionCode": 12345,
       "taskDefinitionCode": 100,    // 同一任务的第二个依赖
       "assetKey": "paimon://ods_db/ods_payment_latest",
       "dependencyGroup": "trade_event_group",
       "conditionJson": {
         "assetKey": "paimon://ods_db/ods_payment_latest",
         "snapshotRequired": true,
         "qualityStatus": "PASSED",
         "schemaStatus": "COMPATIBLE"
       },
       "enabledFlag": 1
     }
   ]
   ```
   
   **Dependency of the DWS layer on the DWD layer**:
   
   ```json
   [
     {
       "workflowDefinitionCode": 12345,
       "taskDefinitionCode": 200,    // DWS_gmv_daily 任务
       "assetKey": "paimon://dw_db/dwd_trade_event_fact",
       "dependencyGroup": "gmv_group",
       "conditionJson": {
         "assetKey": "paimon://dw_db/dwd_trade_event_fact",
         "snapshotRequired": true,
         "qualityStatus": "PASSED"
       },
       "enabledFlag": 1
     }
   ]
   ```
   
   **Trigger behavior**:
   
   1. Flink CDC continuously consumes MySQL binlog and produces an event 
whenever a new snapshot is generated (for example, `ods_order_latest 
snapshot_id=1001`).
   2. AssetEventScanner or the Push endpoint captures the event and writes it 
into `t_ds_asset_event`.
   3. AssetStateService uses optimistic locking to update `t_ds_asset_state`: 
`asset_key=paimon://ods_db/ods_order_latest, latest_snapshot_id=1001`.
   4. DependencyResolver scans via reverse index: which dependency groups are 
associated with this `asset_key`? (for example, `trade_event_group`).
   5. Evaluate all dependencies in `trade_event_group`:
      - Does `ods_order_latest` snapshot exist? ✓  
      - Does `ods_payment_latest` snapshot exist? ✓ (assuming it has also 
advanced)  
      - Are both `qualityStatus` values equal to `PASSED`? ✓  
      → Dependency group READY
   6. Compute 
`trigger_key=workflow_12345_taskdef_100_group_trade_event_group_snapshot_1001_1002`
 (including all key snapshot IDs), and uniquely insert it into 
`t_ds_asset_trigger_history`.
   7. Insert succeeds → the `DWD_trade_event_fact` task instance is notified 
that the dependency is satisfied and executes immediately.
   8. DWD produces a new snapshot (for example, `dwd_trade_event_fact 
snapshot_id=2001`), and steps 2-7 repeat, triggering DWS.
   9. ADS similarly waits for DWS snapshot advancement.
   
   ## 6. Observability Design
   
   ### 6.1 Key metrics (Metrics)
   
   The system should expose the following Prometheus metrics (Micrometer) for 
convenient Grafana dashboard integration:
   
   ```text
   # 事件接入层
   lakehouse_asset_event_received_total{source_type="paimon", asset_key="..."}
     - 意义:从指定资产收到的事件总数
   
   lakehouse_asset_event_deduplicated_total{source_type="paimon", 
asset_key="..."}
     - 意义:被去重丢弃的重复事件数
   
   lakehouse_asset_event_invalid_total{source_type="paimon", asset_key="...", 
reason="xxx"}
     - 意义:被拒绝的无效事件(格式错误、资产不存在等),按原因分类
   
   # 资产状态层
   lakehouse_asset_state_updated_total{asset_key="..."}
     - 意义:资产状态被推进的次数
   
   lakehouse_asset_state_lag_seconds{asset_key="..."}
     - 意义:资产最新快照的年龄(当前时间 - snapshot commit_time)
   
   lakehouse_asset_watermark_lag_seconds{asset_key="..."}
     - 意义:资产 watermark 相对于业务日期的延迟(例如 watermark=10-10 23:59:59,当前日期 10-11,则 
lag=-10秒表示超前)
   
   # 依赖与触发层
   lakehouse_asset_dependency_ready_total{asset_key="...", workflow_code="...", 
dependency_group="..."}
     - 意义:依赖就绪的次数
   
   lakehouse_asset_dependency_blocked_by{asset_key="...", workflow_code="...", 
blocked_by="snapshot|watermark|quality|schema"}
     - 意义:依赖被卡住的原因分布
   
   lakehouse_asset_trigger_success_total{workflow_code="...", 
trigger_source="asset_event"}
     - 意义:由资产事件成功触发的工作流实例数
   
   lakehouse_asset_trigger_failed_total{workflow_code="...", reason="xxx"}
     - 意义:触发失败的工作流数,按原因分类(command_create_failed、duplicate_trigger 等)
   
   # 补偿层
   lakehouse_asset_compensation_scan_total{asset_key="..."}
     - 意义:补偿扫描器运行次数
   
   lakehouse_asset_compensation_missed_events{asset_key="..."}
     - 意义:补偿发现的漏采集事件数
   
   lakehouse_asset_trigger_history_retry_total{trigger_status="..."}
     - 意义:触发历史被重试的次数
   ```
   
   ### 6.2 Key log fields
   
   All log output should include the following contextual fields for end-to-end 
tracing and troubleshooting:
   
   ```json
   {
     "timestamp": "2024-09-10T10:15:30.123Z",
     "level": "INFO",
     "logger": "org.apache.dolphinscheduler.service.asset.AssetEventScanner",
     "traceId": "abc123xyz",
     "assetKey": "paimon://ods_db/ods_order_latest",
     "partitionKey": "dt=20240910",
     "snapshotId": 1001,
     "schemaId": 5,
     "watermark": "2024-09-10T23:59:59Z",
     "eventType": "SNAPSHOT_COMMITTED",
     "eventId": "paimon_evt_20240910_1001_xxx",
     "workflowDefinitionCode": 12345,
     "taskDefinitionCode": 54321,
     "workflowInstanceId": 67890,
     "taskInstanceId": 11111,
     "triggerKey": "workflow_12345_group_gmv_default_snapshot_1001",
     "status": "READY | BLOCKED_BY_QUALITY | TRIGGERED | FAILED",
     "reason": "Asset quality check passed, dependency READY",
     "message": "Asset snapshot processed, dependency READY, triggering 
workflow instance..."
   }
   ```
   
   ### 6.3 Alerting rules
   
   | Alert Condition | Threshold | Priority | Recommended Action |
   |---|---|---|---|
   | `lakehouse_asset_state_lag_seconds > 3600` | 1 hour | P2 | Check whether 
the Paimon table is producing new snapshots; check scanner health |
   | `lakehouse_asset_watermark_lag_seconds > 600` | 10 minutes | P2 | Check 
whether the CDC/Flink source is delayed; check the `_watermark` field in the 
Paimon table |
   | `lakehouse_asset_trigger_failed_total increase > 3/5min` | 3 failures 
within 5 minutes | P1 | Check whether Command creation is failing; check Master 
availability |
   | `lakehouse_asset_dependency_blocked_by{blocked_by="quality"} > 0` 
sustained for > 30min | 30 minutes | P2 | Quality check failure; intervention 
is needed from data producers or the quality team |
   | `lakehouse_asset_compensation_missed_events > 10/hour` | > 10 per hour | 
P1 | Indicates a systemic issue in the Push or Poll path; compensation is 
masking the issue but a root fix is required |
   
   ## 7. Asset Identification Specification
   
   ### 7.1 AssetKey naming convention
   
   In next-generation data warehouses, `assetKey` identifies the globally 
unique location of a Lakehouse table and does not need to include partition 
variables:
   
   ```text
   paimon://<catalog_name>/<database_name>/<table_name>
   ```
   
   Examples:
   
   - `paimon://ods_db/ods_order_latest` — ODS-layer order table
   - `paimon://dw_db/dwd_trade_event_fact` — DWD-layer trade event table
   - `paimon://dws_db/dws_gmv_daily` — DWS-layer daily GMV table
   
   **Notes**:
   - `assetKey` points to the table level and does not include 
`partition_spec`, because in next-generation data warehouses all partitions of 
a table reside in the same Paimon/Iceberg/Hudi primary-key table, and version 
advancement is table-level.
   - If the snapshot independence of different partitions needs to be tracked, 
it should be recorded in the `partitionKey` field of `t_ds_asset_event` (for 
example, `dt=20240910`).
   
   ### 7.2 AssetKey registration and discovery
   
   - Assets must be registered in the `t_ds_asset` table (via API or batch 
scripts), including metadata such as sourceType (paimon/iceberg/hudi), catalog, 
database, and table.
   - When defining workflow dependencies, users query and select existing 
assets through the UI “asset dashboard” or API, similar to the Airflow Dataset 
selector.
   - If a dependent `assetKey` does not exist, DependencyResolver records a 
warning and returns `BLOCKED_BY_UNKNOWN_ASSET`, preventing the workflow from 
being stuck forever without explanation.
   
   ## 8. Phased Implementation Roadmap
   
   ### Phase 1 (MVP, 2–3 weeks): Paimon snapshot -> ASSET_SENSOR single-asset 
trigger closed loop
   
   - Add tables: `t_ds_asset`, `t_ds_asset_event`, `t_ds_asset_state`, 
`t_ds_asset_dependency` (only the scenario where `task_definition_code` is 
non-null), `t_ds_asset_trigger_history`, `t_ds_asset_event_consumer_offset`.
   - Implement `AssetEventScanner` (polling Paimon `$snapshots`).
   - Implement `AssetStateService` (optimistic-lock update and out-of-order 
discard).
   - Implement `DependencyResolver` (support only single-asset snapshot 
existence checks).
   - Implement the `ASSET_SENSOR` task plugin (Strategy A integration).
   - Clarify the MVP problem boundary: prioritize solving the scenario where 
“tasks should trigger on snapshot readiness rather than time,” without changing 
the semantics of existing native DAG dependencies.
   - Complete the end-to-end flow: manually advance a Paimon table snapshot -> 
observe an `ASSET_SENSOR` task transition from waiting to success within tens 
of seconds -> downstream task executes.
   - Complete unit tests for idempotency / out-of-order handling / basic 
observability (log fields; see 5.3).
   
   Key interface sketches:
   
   ```java
   public interface AssetEventSource {
       List<AssetEvent> pollNewEvents(Asset asset, Long sinceSnapshotId);
   }
   
   public interface AssetStateService {
       /** 乱序保护 + 乐观锁更新,返回 true 表示状态被推进 */
       boolean applyEvent(AssetEvent event);
       AssetState getState(String assetKey);
   }
   
   public interface AssetDependencyResolver {
       /** 返回 READY / BLOCKED_BY_WATERMARK / BLOCKED_BY_QUALITY / 
BLOCKED_BY_SCHEMA / WAITING */
       DependencyEvalResult evaluate(AssetDependency dependency);
   }
   
   public interface AssetTriggerService {
       /** 幂等触发,内部完成 trigger_key 唯一插入 + Command 创建或 ASSET_SENSOR 状态回写 */
       TriggerResult triggerIfReady(AssetDependencyGroup group);
   }
   ```
   
   ### Phase 2: multi-asset AND/OR, watermark/quality/schema conditions, 
Compensation Scanner
   
   - Extend `condition_json` to support evaluation of watermark / qualityStatus 
/ schemaStatus.
   - Support multi-asset AND aggregation within `dependency_group` and OR 
across groups.
   - Bring Compensation Scanner online to periodically compare actual Paimon 
snapshots against AssetState, and retry failed triggers.
   - Add observability metrics and Grafana panels.
   
   ### Phase 3: Push reporting API, workflow-level Strategy C, Iceberg/Hudi 
event source integration
   
   - Add `POST /asset-events` API so Flink/Spark jobs can actively report 
events, coexisting with Polling and sharing deduplication.
   - Support workflow-level direct creation of WorkflowInstance (Strategy C), 
and add documentation for rules around bizDate derivation and backfill boundary 
scenarios.
   - Extend the `AssetEventSource` interface with Iceberg/Hudi implementations.
   
   ### Phase 4: governance and observability enhancements (not the core of this 
round, reserved)
   
   - Asset lineage, full asset lifecycle dashboards, cross-project asset 
dependencies, etc.
   
   ## 9. Refactoring and Compatibility for Next-Generation Data Warehouse DAGs
   
   ### 9.1 Relationship with existing cron scheduling
   
   Next-generation data warehouse DAGs are **entirely based on snapshot version 
advancement**, not time triggers. This means:
   
   - **No cron retained**: under the new architecture, a DAG no longer has the 
concept of a “scheduled execution time.”
   - **No time-based dependencies retained**: tasks in the DAG no longer depend 
on the original DEPENDENT model (which targets historical instance results) or 
time dimensions such as “business date.”
   - **Completely driven by data asset versions**: after the DAG starts, 
execution of each task depends 100% on the advancement of upstream asset 
snapshots.
   
   For existing users:
   - To upgrade to the next-generation data warehouse architecture, the DAG 
must be **redefined in full**, replacing every “cron + DEPENDENT” organization 
with “snapshot + assetKey.”
   - This is an architectural upgrade rather than an incremental retrofit, so 
it requires unified planning and execution.
   
   ### 9.2 Registration of the `ASSET_SENSOR` task type
   
   - `ASSET_SENSOR` is a newly added task type and must be registered, 
packaged, and released within `dolphinscheduler-task-plugin`.
   - Existing clusters automatically support it after upgrading to the new 
version.
   - If a workflow definition references `ASSET_SENSOR` but the cluster is not 
upgraded/installed, Master records an error and skips the task.
   
   ### 9.3 Database upgrade path
   
   - New tables such as `t_ds_asset` are automatically created via SQL 
migration scripts in `dolphinscheduler-dao`.
   - The upgrade does not require downtime; migration scripts are idempotent 
and non-invasive to existing data.
   - Compatible with common databases such as MySQL 5.7+ and PostgreSQL 10+.
   
   ### 9.4 Difference from the existing DependentTask
   
   | Feature | DEPENDENT Task (old) | ASSET_SENSOR Task (new) |
   |---|---|---|
   | Dependency object | Upstream task/workflow instance execution result | 
External data asset snapshot version |
   | Applicability | Task orchestration within traditional DAGs | Data 
version-driven scheduling in next-generation data warehouses |
   | Trigger condition | Upstream task success/failure | Asset snapshot 
advancement / quality pass |
   
   ## 10. Frequently Asked Questions (FAQ)
   
   **Q1: How are dependencies on multiple upstream assets handled in a DAG (for 
example, DWS depending on multiple DWD tables)?**
   
   A1: Through `dependencyGroup` combined with AND conditions. All asset 
dependencies within the same `dependencyGroup` must be READY for the group to 
become READY. For example:
   ```json
   [
     { "taskDefinitionCode": 200, "assetKey": "paimon://dw_db/dwd_trade_event", 
"dependencyGroup": "dws_gmv" },
     { "taskDefinitionCode": 200, "assetKey": "paimon://dw_db/dwd_order_dim", 
"dependencyGroup": "dws_gmv" }
   ]
   ```
   Only when both DWD table snapshots advance will DWS be triggered.
   
   ---
   
   **Q2: If an asset event is lost (for example, a Paimon snapshot is not 
captured by the Scanner), will downstream be stuck forever?**
   
   A2: No. There are two layers of protection:
   
   1. Each task has `timeoutMinutes` configured (for example, 60 minutes). 
After timeout, the task enters ERROR state and triggers an alert.
   2. Compensation Scanner runs every 5 to 15 minutes, compares actual Paimon 
snapshots with AssetState, and if it finds missed collections, it backfills the 
event and retriggers evaluation.
   
   ---
   
   **Q3: If multiple workflows depend on the same asset, will they be triggered 
repeatedly?**
   
   A3: No. Each workflow has an independent `trigger_key` for that asset, for 
example:
   ```text
   workflow_A_group_default_snapshot_1001
   workflow_B_group_default_snapshot_1001
   ```
   Although both depend on the same snapshot, their trigger records are 
separate and each triggers once.
   
   ---
   
   **Q4: If a task retries multiple times due to network issues, will it 
repeatedly trigger downstream?**
   
   A4: No. Task instance retry is handled on the Worker side. As long as the 
task eventually succeeds, downstream tasks are released once. Downstream tasks 
are triggered through DAG dependencies and follow the same “task instance 
uniqueness” principle, so they will not be triggered repeatedly.
   
   ---
   
   **Q5: Does it support mixed dependencies across multiple data Lakehouse 
table formats (Paimon, Iceberg, Hudi)?**
   
   A5: MVP (Phase 1) supports only Paimon. Phase 2 extends unified processing 
for watermark/quality/schema conditions. Phase 3 adds Iceberg/Hudi event 
sources. In theory mixed dependencies are supported, but the quality and test 
coverage of the EventSource implementation for each format must be ensured.
   
   ---
   
   **Q6: How are cross-flow dependencies (task dependencies between DAGs) 
supported in the new architecture?**
   
   A6: Cross-flow dependencies are upgraded to “depending on the asset 
snapshots produced by external DAGs.” For example, a task in DAG-B may depend 
on a table snapshot produced by DAG-A:
   
   ```json
   {
     "assetKey": "paimon://dw_db/external_dag_a_output_table",
     "dependencyGroup": "cross_flow",
     "conditionJson": {
       "snapshotRequired": true,
       "qualityStatus": "PASSED"
     }
   }
   ```
   
   This way, DAG-B waits for the external DAG-A table snapshot to advance, 
rather than waiting for a task instance.
   
   ---
   
   **Q7: If INSERT conflicts occur on database unique constraints, will 
exception handling affect performance?**
   
   A7: Database unique constraint conflicts are usually very fast (< 1ms) and 
are part of the expected normal path. They are logged and execution continues, 
with no obvious performance impact. However, if concurrency is very high (for 
example, 10+ Master instances competing to trigger), it is recommended to 
monitor the database connection pool and index performance.
   
   ## 11. Risks and Open Questions
   
   1. **Semantic definition of backfill and event-driven behavior**: manual 
backfill can reproduce historical snapshots, so it must be clarified whether 
those historical snapshots should trigger downstream tasks. It is recommended 
to define a `backfill_mode` marker telling the system whether historical 
snapshots should trigger.
   
   2. **Race window in multi-Master concurrent triggering**: although the 
unique constraint guarantees that only one insert into `trigger_history` 
ultimately succeeds, there is still a short inconsistency window between 
`createCommand` and persistence of `trigger_history`. The ordering constraint 
“insert a placeholder into `trigger_history` first, then call `createCommand`” 
must be explicitly defined, and compensation must be added for `createCommand` 
failures.
   
   3. **Task polling frequency and database pressure**: if many task instances 
poll the same batch of asset states simultaneously, batch-fetching/caching 
strategies must be considered to avoid excessive query pressure on 
`t_ds_asset_state`. It is recommended to introduce an active callback mechanism 
in DependencyResolver to replace per-task polling.
   
   4. **Compatibility of extending `CommandType` semantics**: adding 
`ASSET_EVENT_TRIGGER` requires evaluating its impact on existing code paths 
that exhaustively switch over `CommandType` (such as alerting, logging, and UI 
display). All usages of `CommandType` across the repository should be searched 
and adapted for compatibility.
   
   5. **Permission model for cross-catalog / cross-project asset 
dependencies**: assets may belong to different DolphinScheduler projects or 
even different tenants. It is not yet designed whether dependency declaration 
and triggering require cross-project authorization. This requires separate 
follow-up discussion.
   
   6. **Clock/order dependence in deduplicating Push and Poll paths**: if the 
same snapshot event arrives through the two paths within an extremely short 
interval, the unique constraint prevents duplicate persistence, but correctness 
and performance of database unique-index conflict handling under high 
concurrency still need to be validated.
   
   7. **Scope of exactly-once claims**: this design can guarantee only that 
“the trigger record for the same `trigger_key` is successfully created once,” 
but cannot guarantee strict exactly-once semantics for downstream Command 
consumption and WorkflowInstance creation under extreme failure scenarios. This 
boundary must be explicitly documented.
   
   8. **Mixed dependencies between partitioned and non-partitioned tables**: 
when a task in the DAG simultaneously depends on snapshots from partitioned and 
non-partitioned tables, how to express and evaluate them uniformly remains 
open. Clear rules need to be defined in dependency expressions.
   
   ## 12. Summary
   
   This design upgrades DolphinScheduler from a “time-based DAG scheduling 
system” to a “data asset version-driven scheduling system.” The core principles 
are:
   
   - **The entire workflow (DAG) represents the full data processing chain from 
ODS -> DWD -> DWS -> ADS**.
   - **The trigger of every task layer in the DAG is entirely determined by the 
advancement of snapshot versions of its dependent assets**, rather than time or 
manual triggering.
   - **By introducing the new `ASSET_SENSOR` task type**, the existing task 
instance state machine and DAG execution mechanism are reused.
   - **By introducing new asset tables and an event deduplication mechanism**, 
idempotent triggering and multi-Master concurrency safety are guaranteed.
   - **Without breaking the existing DolphinScheduler kernel**, integration is 
achieved through plugin-based tasks and the existing Command entry point.
   
   This is a native scheduling solution for next-generation Lakehouse 
architectures (Paimon/Iceberg/Hudi), and it addresses the root problem of 
mismatch between traditional time-based triggering and actual data availability.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: 
[email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to