- 数据存储
- 后端
【免费下载链接】opendal
Apache OpenDAL: One Layer, All Storage.
Apache OpenDAL 的规范(Specifications)定义了它在所有支持服务上承诺的可移植行为:这些跨服务成立的"可观察契约"以活文档形式存在,要求实现、API 文档与行为测试(behavior tests)始终与规范保持一致。本篇以仓库中 core/core/src/docs/specs/README.md 为主体,完整展开规范的定义、治理流程与两份现行规范(条件操作、Metadata)的内容,并结合 Capability 结构体、错误类型 与 behavior 测试 等源码实现,说明"规范—能力—测试—文档"这条契约链在 OpenDAL 中如何闭环落地。
什么是 OpenDAL Specifications:活文档与可移植契约
规范文档开篇给出了 specifications 的核心定位:
Specifications define the portable behavior OpenDAL promises: the observable contracts that hold across every supporting service. They are living documents; implementations, API documentation, and behavior tests must remain consistent with them as OpenDAL evolves.
从中可以提炼出四个关键属性:
可移植契约(portable contract):规范只承诺"在每一个支持该能力的服务上都成立"的可观察行为。调用方依赖的必须是契约内行为;契约之外的行为不属于可移植保证,调用方不应假设它跨服务一致。
活文档(living documents):规范描述的是"当前契约"。随着实现演进,规范会随之更新,而不是冻结在某次设计的状态。
能力门控(capability-gated):部分承诺仅在服务声明(advertise)了对应 capability 时生效;对不支持的请求,服务返回显式错误,而不是静默降级。
三方一致性:实现、API 文档、行为测试必须与规范保持一致。README 明确指出:
- 规范声明契约(state the contract);
- core/tests/behavior 下的行为测试为契约提供"可执行的符合性覆盖"(executable conformance coverage);
- 公共 API 文档在 API 使用现场解释可观察行为、能力要求、参数含义和相关错误,然后链接到规范以获得完整的跨操作契约。
规范与 RFC 的分工:当前契约 vs 不可变历史
README 中一段容易被忽略但极其重要的区分是规范与已接受 RFC 的关系:
Accepted RFCs are immutable historical documents. They record the design that reached consensus, including its original motivation and trade-offs. Specifications describe the current contract, even when later implementation work or a newer RFC changes the accepted design.
也就是说:
- RFC(位于 core/core/src/docs/rfcs/,如 RFC 8147 "Write If Not Changed")是不可变的历史文档,记录达成共识时的原始动机与取舍,即使后续实现改变了该设计,RFC 原文也不改;
- Specification描述当前契约,是活文档。
这对读者的实用意义在于:查"当初为什么这么设计"去读 RFC;查"现在行为是什么、能依赖什么"读 specification,以规范为准。
规范的治理流程:PR 与 RFC 两条路径
README 还定义了规范变更的两条路径:
- 编辑性修正(Editorial corrections)与固化已实现行为:走普通 pull request 流程,并附带该行为的证据(实现、测试);
- 实质性契约变更(Substantial contract change):必须发起新的 RFC。且变更后,实现、行为测试、API 文档、规范四者必须一起落地(land together)——不允许"规范先改、实现后补"或"实现先上、文档欠账"的状态。
现行规范一览:条件操作与 Metadata
README 列出了两份 active specifications(原文为指向站点页面的外链,仓库内的权威来源是以下两份 Markdown 文件,由 core/core/src/docs/specs/mod.rs 通过include_str!直接编入 rustdoc):
| 规范 | 仓库内源文件 | 核心内容 |
|---|---|---|
| Conditional operations | conditional-operations.md | 定义可移植的谓词、错误与原子性语义,包括文件不存在时的行为 |
| Metadata | metadata.md | 定义 owned metadata、紧凑构造上限与结果完成(result completion)语义 |
mod.rs中的组织方式值得注意:pub mod conditional_operations与pub mod metadata两个空模块各自挂载了对应 Markdown 的 rustdoc,使规范成为 Rust API 文档的一等公民——开发者在rustdoc中查任何条件操作选项时,都能被引到这份跨操作契约上。
规范一:条件操作(Conditional Operations)
条件操作允许调用方"只有当文件处于预期状态时,才观察或变更存储"。规范全文(conditional-operations.md)可拆为六个问题:条件检查的是哪个文件、能力契约、文件存在/不存在时的行为、if_not_changed的降级(lowering)、错误契约、原子性与可见性。
条件检查的是哪个文件
规范首先澄清"每个条件只检查一个文件",并规定了不同操作下该文件是谁:
stat与read检查被观察的文件;write与delete检查操作路径上的文件;copy与rename检查目标文件(destination);restore检查当前存储在 restore 路径的文件,而所选版本仍然是 restore 的来源。
规范特别区分了两个易混淆概念:version selector选择操作作用于哪个已存储版本;version condition比较的是"条件所检查的那个文件"的版本。二者是不同概念。
另一个明确声明的未定义区:copy/rename 可能因为源不存在而失败,restore 可能因为所选版本不存在而失败;当这类错误与条件错误同时发生时,规范不定义操作返回哪个错误——调用方不应依赖这种组合下的具体返回值。
能力契约:OpenDAL 永不静默忽略条件
规范中"Capability contract"一节与源码中的 Capability 直接对应:
- OpenDAL 在分发操作之前校验条件选项;若服务未声明所需 capability,操作返回
Unsupported。OpenDAL 永远不会静默忽略一个条件。 - 声明了某个条件能力的服务,必须精确实现该条件的契约。
- 独立的能力字段不意味着支持这些条件的组合。若实现接受组合,文件必须满足所有条件;若无法保持完整的合取(conjunction),必须返回
Unsupported,而不是丢弃某个条件。
这一点在Capability结构体中体现得非常具体:它按"操作_条件"的命名模式为每个组合单独设布尔字段,例如stat_with_if_match、write_with_if_not_exists、copy_with_if_not_exists、rename_with_if_not_exists、delete_with_if_version_not_match等(见 capability.rs)。字段注释也明确写道:"Conditional capability fields follow the conditional operation specification"——能力字段本身就是规范的镜像。
文件存在时的条件语义表
规范给出了一张完整的条件真值表(stat/read/write/delete/copy/rename/restore通用,以"文件存在"为前提)。注意规范强调:表中 ETag 条件使用具体 ETag 值;通配符 ETag(wildcard ETag)除非某操作的具体文档另行定义,否则没有可移植含义。
| Condition | 成功条件 |
|---|---|
if_match(etag) | 文件的 ETag 等于etag |
if_none_match(etag) | 文件的 ETag 不等于etag |
if_version_match(version) | 文件的版本等于version |
if_version_not_match(version) | 文件的版本不等于version |
if_not_exists() | 永不成功(文件存在即条件不成立) |
if_modified_since(time) | 文件的修改时间在time之后 |
if_unmodified_since(time) | 文件的修改时间不在time之后 |
if_not_changed(metadata) | 文件具有metadata所表示的身份 |
文件不存在时的条件语义表
文件不存在时的行为是各云服务实现差异最大的地方,规范为此给出了明确的可移植表:
- 对
stat和read:无论带什么条件,都返回NotFound。 - 对变更类操作(mutations):
| Condition | 结果 |
|---|---|
if_match(etag) | ConditionNotMatch |
if_version_match(version) | ConditionNotMatch |
if_not_changed(metadata) | ConditionNotMatch |
if_none_match(etag)(具体 ETag) | 条件成功 |
if_not_exists() | 条件成功 |
if_version_not_match(version) | 未定义可移植行为 |
条件成功后,操作按正常语义执行:删除一个不存在的文件仍然是成功的 no-op;write/copy 可以创建文件;rename 和 restore 仍可能因为源或所选版本不存在而失败。
if_not_changed的降级(lowering)规则
if_not_changed(metadata)是一个正身份条件,来源于此前 OpenDAL 返回过的Metadata。规范定义了确定性的五步降级过程:
- 若操作声明支持 version matching,且 metadata 中包含 version,使用 version match;
- 否则,若 metadata 包含 ETag,使用 ETag match;
- 否则,若 metadata 包含 version,使用 version match;
- 若 metadata 两者都不含,返回
ConfigInvalid; - 按正常流程校验推导出的原始条件,若操作不支持则返回
Unsupported。
组合语义:与推导条件完全相同的显式相等条件是冗余的;与之冲突的显式相等条件返回ConditionNotMatch;其他被接受的条件与推导条件按AND组合。
降级发生的时机也被精确约束:发生在"可变公共 options 仍持有源 metadata、且 OpenDAL 已知组合后服务 capability"的阶段;最终固化的 raw 操作参数(Op*)中只含被选中的原始相等条件,不再保留源Metadata。
这部分在源码中有直接印证:
- core/core/src/types/options.rs 中
WriteOptions、DeleteOptions、CopyOptions等的if_not_changed: Option<Metadata>字段携带公共 options 阶段的可变 metadata; - core/core/src/raw/ops.rs 的测试
options_lower_if_not_changed_before_freeze验证了降级行为:当 capability 同时支持write_with_if_match和write_with_if_version_match、metadata 同时含 version 与 ETag 时,OpWrite优先降级为if_version_match;当 capability 只有delete_with_if_match时,OpDelete降级为if_match; - 公共 API 上,
if_not_changed以 builder 方法暴露,例如 operator_futures.rs 中的WriteOptionsFuture::if_not_changed(metadata: &Metadata),文档注释统一指向规范:"Refer tooptions::WriteOptions::if_not_changedfor the full contract"。
错误契约与协议状态码的边界
规范定义了四类错误及其边界:
| 情形 | 错误 |
|---|---|
| 显式条件求值为假 | ConditionNotMatch |
stat/read观察到不存在的文件 | NotFound(与条件无关) |
| 服务自报的、与 OpenDAL 显式条件无关的冲突 | Conflict |
| 条件或条件组合不可用 | Unsupported |
关键细节是:协议状态码本身不决定错误类别。例如 HTTP304 Not Modified或412 Precondition Failed,只有当它是服务对"OpenDAL 显式提供的条件"的响应时才映射为ConditionNotMatch。
这与 core/core/src/types/error.rs 中ErrorKind的定义一一对应:ConditionNotMatch的文档注释明确写道"仅对调用方通过 OpenDAL options 提供的条件返回;服务侧自报冲突返回Conflict;用stat/read观察不存在的文件无条件返回NotFound",并链接回条件操作规范。此外ConfigInvalid的注释也点明了if_not_changed场景:"metadata 中没有可用 version 或 ETag"正是ConfigInvalid的典型来源之一。
一个实践示例来自 core/tests/behavior/async_write.rs:test_write_with_if_not_exists先检查op.info().capability().write_with_if_not_exists,不支持则跳过;随后对已存在的路径执行.if_not_exists(true)写入并断言得到ConditionNotMatch。这就是"能力门控 + 条件错误契约"在测试中的落地形态。
原子性与可见性:禁止 stat-then-act
规范对原子性提出了最硬性的要求:
- 服务只有在能够把条件判定与变更的可见提交(visible commit)原子化时才允许声明变更条件能力。实现不得用"先
stat再无条件变更"来模拟条件变更(stat-then-act 反模式被明确禁止)。 - 对分阶段或多段写入(staged/multipart),条件在最终提交时判定;条件失败时,先前可见的文件保持不变。
- 条件错误在哪个环节暴露是未指定的:可能在构造 I/O 句柄时、I/O 过程中、或关闭句柄时;但最终结果与原子性契约不变。
- 条件批量删除逐条独立判定每个条目;只有当服务能保持每个条目的条件与错误时才允许批量处理。部分成功的详细报告遵循 delete 操作自己的契约。
规范二:Metadata 契约
metadata.md 定义Metadata的可移植语义,要点如下:
Owned、不可变、可任意生命周期持有
Metadata是对一个存储条目的自有(owned)、不可变描述。它不借用自 response、lister、reader 或 operation,因此调用方可以以任何生命周期持有它;克隆之间可以在内部共享不可变存储,而对某一个 clone 执行Metadata::into_builder并构建修改值,不会影响其他 clone。
构造与访问
- 构造入口:
MetadataBuilder::file(content_length)、MetadataBuilder::dir()、MetadataBuilder::unknown(),最后以MetadataBuilder::build完成; - 已有 metadata 可通过
Metadata::into_builder消费,再用set_file(content_length)、set_dir()、set_unknown()修改 mode 后重建; - 用户元数据(user metadata)通过借用视图
UserMetadata暴露,支持查找、长度检查与迭代,且不需要HashMap分配;"未提供的 user metadata 映射"与"显式提供的空映射"保持可区分。
对应实现位于 core/core/src/types/metadata.rs:into_builder(L203)、file(L356)、dir(L364)、unknown(L370)、build(L496)。
紧凑构造上限:u16::MAX
Rust 侧 metadata 与固化后的操作参数使用紧凑块(compact blocks)表示,含索引在内的编码尺寸不得超过u16::MAX字节;超出即 panic。规范同时提醒:各服务与调用方应在此表示边界之前先执行自己更小的协议限制。源码中MetadataBuilder::build的文档注释与断言(total_len <= u16::MAX as usize,见 metadata.rs)与此完全一致。
Content length 的完成语义
规范对"每个文件 Metadata 必然含完整对象长度"给出了构造保证与提升(promote)规则:
MetadataBuilder::file(0)表示空文件;目录与 unknown metadata 报告零;- Raw 服务与中间层在"条目 mode 或完整长度任一不可用"时使用
Unknown,并只在拥有权威长度的边界上提升为FILE:- stat 与 read 响应解析器只有在从服务响应拿到完整对象长度后才构造
FILE; - list 完成时统计 unknown 条目、解析其 mode,并用 stat 结果的长度提升文件;
- write 完成时替换写入用已写字节数;append 适配器使用最终对象偏移而非追加字节数;
- copy 完成时使用权威的服务结果或(若 copy 协议提供)成功拷贝进度;否则信任调用方断言的源长度(不读取源 metadata),无断言时才在拷贝前读源 metadata。它不会对目标发
stat,且当上述来源都无法提供权威长度时失败; - 不携带对象长度的 delete marker 使用 content length 零。
- stat 与 read 响应解析器只有在从服务响应拿到完整对象长度后才构造
操作边界(Operation boundary)
公共操作 options 是可变的逻辑构造值;OpenDAL 在把它们冻结为 rawOp*参数之前,会针对组合后的服务 capability 解析if_not_changed等逻辑条件。raw 参数中只保留服务必须执行的原始条件,不保留源Metadata——这正是前文if_not_changedlowering 一节所述的机制,两份规范在此交汇。
符合性验证:Capability 门控的行为测试
规范"Conformance"一节承诺:core/tests/behavior 下按 capability 门控的行为测试会针对真实服务验证可移植条件,且只覆盖本规范定义的行为,不施加规范之外的跨服务期望;服务在声明条件能力之前,必须先通过相关的可移植行为测试。
这套机制的运行方式(来自 core/tests/behavior/README.md):
- 用
OPENDAL_TEST环境变量选择被测服务,例如:
OPENDAL_TEST=fs cargo test behavior --features tests- 运行某一类测试或单个测试:
OPENDAL_TEST=fs cargo test behavior::test_write --features tests OPENDAL_TEST=fs cargo test behavior::test_stat_dir --features tests- 测试按 operator 的完整 capability选择;可用
OPENDAL_TEST_CAPABILITY_OVERRIDES覆盖 capability 做针对性验证:
OPENDAL_TEST=memory OPENDAL_TEST_CAPABILITY_OVERRIDES=stat_with_version=false,read_with_version=false,delete_max_size=700 \ cargo test behavior --features tests- 环境变量未设置时所有行为测试默认跳过,适合本地按需启用。
条件操作相关的测试分布在 async_write.rs、async_read.rs、async_copy.rs、async_rename.rs 等文件中,模式统一:先检查op.info().capability().*_with_*能力字段,不支持即跳过,支持则执行条件操作并断言ConditionNotMatch等规范约定的错误——这正是"规范 → capability 字段 → 行为测试"三方一致的例证。
文档链路:规范如何被分发
- rustdoc:core/core/src/docs/specs/mod.rs 用
include_str!把README.md作为docs::specs模块文档,把两份规范挂载为conditional_operations与metadata子模块文档,开发者在 API 文档内即可直达完整契约; - 官网文档:website/docs/04-specifications/01-overview.mdx 等页面通过 frontmatter 中的
specification_source指向本 README 与两份规范源文件,站点内容由仓库内的权威 Markdown 生成; - API 现场:每个条件选项(
if_match、if_not_exists、if_not_changed等)的 rustdoc 在解释可观察行为与能力要求后,链接回规范,形成"API 文档讲现场行为、规范讲跨操作契约"的分工。
历史决策(Historical decisions)
规范末尾以"Historical decisions"一节保留了到达当前契约的设计轨迹,体现了"RFC 不可变、规范随当前契约演进"的治理原则。条件操作规范引用的 RFC 包括:
- RFC 5485: Conditional Reader(条件读取的起源)
- RFC 7818: Rename If Not Exists
- RFC 8145: Conflict Error Kind(
Conflict错误类别的引入,与ConditionNotMatch的边界划分) - RFC 8147: Write If Not Changed(
if_not_changed写入条件)
Metadata 规范则引用了 RFC 8194: Compact Metadata and Operation Arguments(紧凑 metadata 与操作参数的设计)及其跟踪 issue。这些 RFC 文档保存在仓库的core/core/src/docs/rfcs/目录中(如 8147_write_if_not_changed.md、8194_compact_metadata_and_operation_args.md),需要追溯设计动机时可按图索骥。
小结
OpenDAL 的规范体系可以概括为一句话:规范声明契约,capability 声明边界,行为测试验证符合性,API 文档在使用现场指回规范。对使用者的实际价值是:只要服务声明了相应 capability,你从规范表格中读到的每一行条件语义、每一条错误映射、每一项原子性承诺都是可依赖的跨服务保证;而ConditionNotMatch、Conflict、NotFound、Unsupported、ConfigInvalid这几个错误类别的边界,在规范与 ErrorKind 源码中是逐字一致的。要深入某一项契约,建议的阅读路径是:本 README 建立整体框架 → 对应规范文件读具体语义表 →capability.rs查服务实际声明 → behavior 测试看可执行验证。
- 数据存储
- 后端
【免费下载链接】opendal
Apache OpenDAL: One Layer, All Storage.
相关推荐
Agent Zero 测试体系与 tests/AGENTS.md 契约:pytest 回归、安全与契约测试的工程规范
Agent Zero 测试体系与 tests/AGENTS.md 契约:pytest 回归、安全与契约测试的工程规范 本文基于 Agent Zero(Pytho
人工智能大模型AI AgentAgent 框架自主智能体多智能体工具调用MCP 服务浏览器控制DeepSeek Harness 可移植标识符接缝:代码运行时共享绑定排除契约的设计与落地
DeepSeek Harness 可移植标识符接缝:代码运行时共享绑定排除契约的设计与落地 DeepSeek Harness(Everything is a P
人工智能AI AgentAgent 框架DeepSeekApache OpenDAL RFC-3017 解析:为何移除 `oio::Write::copy_from` 及其设计演进
Apache OpenDAL RFC 3017 解析:为何移除 oio::Write::copy_from 及其设计演进 导读 本文以 Apache OpenD
数据存储后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考