Apache Airflow ADR-0006 解析:为什么 `@task.stub` 混合语言 DAG 不在 Code 视图中展示 Lang-SDK 源码
导读
在 Apache Airflow 的 Lang-SDK(语言 SDK,当前落地为 Java SDK)架构中,用 @task.stub 声明的 Java/Go 任务与 Python 任务共存于同一个 DAG(即“混合语言 DAG”)。此时用户会在 Airflow UI 的 Code 页看到一份代码——本文依据仓库中的架构决策记录 ADR-0006(airflow-core/adr/lang-sdk/ 目录)展开,说明一个看似“缺功能”实则是有意为之的设计结论:Airflow 不会在 UI 中展示混合语言 DAG 的 Lang-SDK 侧源码,Code 视图只显示包含 stub 声明的 Python DAG 文件。读完本文,你将理解这项“负面决策”背后的四条工程理由、被否决的三种备选方案(含完整的设计草图)、它对 DagCode 表结构与 /dagSources REST 契约的现状约束,以及它如何与纯 Java DAG 的源码打包机制(ADR-0003)相互独立、各司其职。
ADR-0006 的定位:记录一项“不做”的决策
ADR-0006 是一份 Architecture Decision Record(架构决策记录),状态为 Accepted(已接受)。它的特殊之处在于记录的是一个 negative decision(否定性决策)——正如 ADR 顶部注释所写:
这份 ADR 记录了一项否定决策。它存在的意义是:今后任何提议为 stub 支撑的 DAG 提供多文件/多语言源码展示的功能请求或 PR,都可以直接指向本文,而不必重新讨论设计。
仓库的 lang-sdk ADR 目录 统管“在 Airflow 中运行非 Python 任务”(AIP-108)所涉及的横切架构决策,包括协调器层、工作负载执行、打包方式,以及语言 SDK 触碰 Airflow 核心表面(DAG 解析、DagCode、REST API 与 UI)的方式。这些决策原本在 java-sdk/adr/,后经评审迁移到核心侧,因为它们约束的是核心接口,且适用于所有语言 SDK 而非仅 Java SDK。整个系列包括:
- ADR-0001:Java SDK 与 Airflow 集成及协调器扩展点
- ADR-0003:纯 Java DAG——构建期打包与代码可见性
- ADR-0004:语言特定 DAG 文件解析
- ADR-0005:协调器打包、模块布局与注册
- ADR-0006:混合语言(
@task.stub)DAG 不展示 Lang-SDK 源码 - ADR-0007:跨语言边界的 TaskFlow 参数绑定
而仅针对单一 SDK 的决策仍留在对应 SDK 目录中(如 Go SDK 的 bundle 格式决策位于 go-sdk/adr/)。
背景:什么是混合语言(@task.stub)DAG
按 AIP-108 的当前范围,Java 任务以 @task.stub 操作符的形式声明在普通的 Python DAG 文件中:DAG 本体用 Python 定义,任务实现则位于随 bundle 一起分发的 Java-SDK 构建产物(JAR)里。这就是 ADR-0006 所称的 mixed-language Dag(混合语言 DAG)。
一个典型的混合语言 DAG 如下(示例源自 ADR-0001):
@task()
def python_task_1():
return "value_from_python_task_1"
@task.stub(queue="java")
def extract(): ...
@task.stub(queue="java")
def transform(): ...
@task()
def python_task_2(transformed):
print(transformed)
@dag(dag_id="java_interface_example")
def simple_dag():
python_task_1() >> extract() >> transform() >> python_task_2()
关键点在于:@task.stub 的声明不带任何 Python 实现,执行被委托给该任务 queue 所对应的协调器(coordinator)。从数据模型上,stub 是一个被标记的“非映射操作符”——在核心序列化层,操作符上存在 is_stub: bool = False 字段(见 baseoperator.py),序列化 DAG 时若操作符带 is_stub,则会调用 stub_arg_bindings.py 在 DAG 序列化期物化其有序位置参数绑定规格(build_arg_bindings),供远端运行时解码(见 serialized_objects.py)。
与之相对的是 纯 Java DAG(ADR-0003):DAG 本体用 Java 定义,协调器通过 Airflow-Java-SDK-Dag-Code manifest 属性解包内嵌的 .java 源码用于展示。该机制随纯 Java DAG 创作一并被移出了 AIP-108 的范围——当前 AIP-108 下 Java 只能以 stub 任务的形式参与 DAG,纯 Java DAG 留待后续(很可能在 AIP-85 稳定之后)再提。
曾被提出但被否决的设计草图
在 AIP-108 的测试与演示期间,用户提出:混合语言 DAG 的 UI Code 视图能否在包含 stub 声明的 Python 文件之外再展示 Java-SDK 侧的源码——例如在 Python 源码旁边加一个第二标签页。对应追踪问题在 ADR 中被记为 #67260(该问题最终以 won't-fix 关闭并指向本文)。
一份完整的设计草图当时被起草,以扩展 AIP-85 的 DagImporter 接口来实现,包含四个组成部分:
StubDagImporter:继承自 Python importer 的子类,在解析后定位每个 stub DAG 在 bundle 中的支撑产物(artifact)并捕获其源码。get_source_code变为“每个 DAG、多文件”语义:签名从现有形式扩展为get_source_code(file_path, dag_id, *, bundle_path, bundle_name) -> dict[str, DagSource], 使定义在同一个 Python 文件中的多个 DAG 不会把彼此的 artifact 泄漏进各自的 Code 视图。- 数据库 schema 变更:允许每个
DagVersion对应多行DagCode——移除DagCode.dag_version_id上的唯一约束,新增source_file_name、language与一个入口点标记列;artifact 摘要(digest)参与dag_hash,使 JAR 重新构建后产生新的DagVersion。 - REST API 与 UI 变更:
GET /dagSources/{dag_id}增加“文件”维度;Code 页增加文件标签栏,按文件标记语言,取代硬编码的 Python。
决策:不支持,Code 视图维持单文件现状
经过讨论,社区决定不推进上述方案。ADR-0006 的正式决策是:
我们不支持查看混合语言 DAG 的 Lang-SDK 侧源码。 对于包含由 Lang-SDK artifact 支撑的
@task.stub任务的 Python DAG,Code 视图只展示 Python DAG 文件——即现有的“单源码、按DagVersion”行为。
具体而言,以下内容均未实现:
- 一个能解析并捕获 stub 支撑 DAG 的 Lang-SDK artifact 源码的
StubDagImporter(或CrossLangDagImporter); get_source_code的“每个 DAG、多文件”语义;- 每个
DagVersion对应多行DagCode,或DagCode上新增source_file_name/language/ 入口点标记列; GET /dagSources/{dag_id}上的文件维度,或针对该用例的多文件 Code 页;- 仅为“源码展示”目的而把 artifact 摘要盖到序列化 DAG 模型上(artifact 摘要是否应参与 DAG 版本化以保障执行正确性,是另一个问题,超出本 ADR 范围)。
这一现状在仓库核心代码中有清晰印证:
dag_code表一行对应一个 DAG 版本:模型 dagcode.py 中,dag_code表将source_code存为单列Text(MySQL 上为MEDIUMTEXT),并带source_code_hash;其外键列dag_version_id声明了unique=True(见 dagcode.py),即同一 DAG 版本至多一行代码。DagVersion侧的回填关系dag_code同样以uselist=False建模(见 dag_version.py)。- 没有语言/文件名维度:
/dagSourcesAPI 的响应模型DAGSourceResponse只有content、dag_id、version_number、dag_display_name四个字段(见 dag_sources.py 数据模型),不存在language或source_file_name。 - REST 契约单文件:公共路由
GET /api/v2/dagSources/{dag_id}(见 dag_sources.py 路由)返回单份内容,支持 JSON 或纯文本两种 Accept。 - UI 的 Code 页按 DAG 版本取单份源码:页面组件 Code.tsx 通过
useDagSourceServiceGetDagSource拉取所选DagVersion对应的源码,配合DagVersionSelect做版本切换,用 Monaco Editor 展示单一文件——没有多文件标签栏。
另外,源码展示还受权限控制:无权限时路由返回 REDACTED - you do not have read permission on all Dags in the file(见 dag_sources.py 路由),并依赖 requires_access_dag(... access_entity=DagAccessEntity.CODE) 做鉴权。
为什么不做:四条工程理由
ADR-0006 给出了四点理由,按优先级逐条拆解如下。
1. 含 stub 任务的 DAG 并不是一种独立的 DAG 类别
stub 操作符只是普通 Python DAG 里的又一个操作符。一个 DAG 很可能在近期就会混合多种语言的任务,因此并不存在一个原则上可界定、有界的“另一侧文件”集合。如果围绕“特殊的 stub DAG”概念去设计 importer 接口,等于把一个人为的区分固化进 AIP-85 的 DagImporter 契约——为将来埋下一个需要长期维护的错误抽象。
2. 它最终会退化成在 dag_code 里存一棵目录树
要对 Java 侧展示任何“真正有用”的内容,都意味着不止一个文件——尤其 Java 代码习惯把每个任务拆到各自的小文件/类中。照此思路推演到底,就是把整棵目录树持久化到 dag_code 表——而这并非该表的职责。dag_code 的设计定位是“同步由调度器处理的 DAG 文件代码”,每次写入读取单份源码文件,并计算 md5 哈希(dag_source_hash,见 dagcode.py),其语义与“存储一整个源文件树”格格不入。
3. 源码视图在纯 Python 场景下本就是尽力而为(best-effort)
由工厂函数生成的 DAG,其 Code 视图展示的本来就是一个不含“真正定义”的文件。混合语言 DAG 只是这个已知、被接受的限制的又一实例,而非必须弥补的新缺口。换句话说,Airflow 的源码展示从未承诺“所见即真实定义”,因此混合语言场景并不构成回归。
4. 该问题是暂时性的,且将变得罕见
一旦 DAG 可以原生地用其他语言创作(即 ADR-0003 / ADR-0004 所述的纯 Java DAG,很可能在 AIP-85 稳定之后),在乎 UI 中查看 Lang-SDK 代码的作者可以直接编写原生 Lang-SDK DAG——其入口源码会经由普通的单文件路径作为该 DAG 的源码被展示。为一个持续缩小的边缘场景,把 DagImporter 接口、DagCode schema、版本化写入路径以及 REST/UI 契约全部拧成死结,是一笔糟糕的交易。
备选方案对比
ADR 记录了三个被评估的选项:
| 方案 | 结论 | 理由 |
|---|---|---|
完整提案(每 DAG 多文件 get_source_code + 每 DagVersion 多行 DagCode + REST/UI 变更) |
拒绝 | 接口与 schema 复杂度、原子多行写入语义、artifact 哈希耦合进 dag_hash——全部只为罕见且暂时的用例 |
| 仅展示 Lang-SDK 入口文件(作为第二标签页) | 拒绝 | Java 代码往往分布在众多小文件中,仅展示入口价值有限,却仍需要几乎全部相同的 schema、API 与 UI 改动 |
| 维持现状——只展示 Python 文件 | 接受 | 见上节四条理由 |
结论与实际影响
对架构各层的影响:一切保持不变
- AIP-85 的
DagImporter接口、DagCodeschema(每个DagVersion一行)、/dagSourcesAPI 契约以及 Code 页 UI 全部保持原样; - 无需为源码展示而构建、测试或维护跨语言源码对账(cross-language source reconciliation)、多行原子写入语义或由 artifact 哈希驱动的
DagVersion更替。
对混合语言 DAG 用户意味着什么
使用混合语言 DAG 的用户,在 Code 视图中只会看到 Python stub 声明,要查看 Java 实现必须回到自己的仓库或构建产物。ADR 特别注明:Java SDK 的用户文档会显式声明这一行为,使其成为预期行为而非被上报的缺陷。
这与 Dag 版本化机制的配合也值得注意:每当 DAG 文件变化(内容或 fileloc 变化),dag_code 中的行会被写入或更新(write_code / update_source_code,见 dagcode.py 与 dagcode.py);而每次解析会由 DagVersion.write_dag 递增 version_number(见 dag_version.py,配合 (dag_id, version_number) 唯一约束)。Code 页的版本选择器正是基于这套 dag_version 历史,让用户能回看某个版本的 Python 声明文件——但无论哪个版本,它都只对应一份 Python 源码。
与 ADR-0003 源码打包机制的关系:互不影响
ADR-0003 的源码打包机制(Airflow-Java-SDK-Dag-Code manifest 属性)不受本决策影响:如果未来某个后续提案重新启用纯 Java DAG 创作,该机制依然是展示原生创作的 Lang-SDK DAG 源码的方式。也就是说,Airflow 存在两条并行的源码可见性路径:
- 混合语言 stub DAG(当前 AIP-108 范围):Code 视图 = Python 声明文件,Java 实现看仓库/构建产物;
- 原生 Lang-SDK DAG(ADR-0003,暂移出范围):构建期将
.java入口源码打进 JAR,协调器解包后用现有DagCode基础设施存储与展示。
实践建议与 FAQ
Q:我在 UI 中看不到我的 Java 任务实现,是出 Bug 了吗?
A:不是。对含 @task.stub 的混合语言 DAG,这是 ADR-0006 明确的预期行为——Code 视图按设计只显示 Python DAG 文件。
Q:为什么不能用“再加一个标签页”这样简单的方案? A:Java 源码以多文件、每任务一类的形态存在,单入口文件价值有限,却几乎触发与完整方案等量的 schema/API/UI 改动。权衡后被否决。
Q:如果我希望在 Airflow UI 中看到 Lang-SDK 侧源码,该怎么办? A:两条路径:其一,在仓库中查看对应语言 SDK 的任务源码(构建产物 JAR 内的实现);其二,等待纯 Java(原生语言)DAG 创作能力在未来被重新纳入范围——届时该 DAG 的入口源码将通过普通的单文件路径展示。
Q:这个决策会锁死未来的演进吗? A:不会。作为负面决策记录,它的作用是让未来同类提议直接引用本文、避免重复论证;若 AIP-85 稳定后 AIP-108 范围再次扩展,纯语言 DAG 路径将天然解决源码可见性诉求。
进一步阅读
围绕本文涉及的源码展示与版本化链路,可在仓库中继续深入:
- 决策原文:ADR-0006 及其所属 lang-sdk ADR 系列
- 关联决策:ADR-0003 纯 Java DAG 的构建期打包与源码可见性、ADR-0001 Java SDK 集成与
@task.stub、ADR-0004 语言特定 DAG 解析 - 数据模型现状:DagCode 模型、DagVersion 模型
- API 现状:dag_sources 公共路由、DAGSourceResponse 数据模型
- UI 现状:DAG Code 页组件
- stub 序列化细节:stub_arg_bindings.py
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00