0.4 升级须知¶
0.4 是 0.x 阶段的一次不兼容升级:规范文档、Python SDK(dpe-hash / dpe-sdk)与 Rust SDK(dpe-hash / dpe-sdk)一起从 0.3 升到 0.4.0,跑分器 dpe-conformance 随 0.4.0 首次发布。本页以已发布的 0.3.1(文档 v0.3.1、py-v0.3.1、rs-v0.3.1)为基线,分两部分:0.4 首次提供的能力(新增,无需迁移),以及相对 0.3.1 的变化(需要按「要做什么」处理)。细节见对应 Issue / PR。
总体结论¶
- hash 值不变:自 0.3.1 以来,
vectors/中 dpe1 的全部期望值没有变化。已落库的数据不需要重算,升级不会引起重学。 - 依赖范围:下游按
>=0.4,<0.5声明依赖(文档与两个 SDK 保持相同的X.Y)。
0.4 首次提供的能力¶
以下能力在 0.3.1 中没有发布,0.4 是首个版本,不存在迁移问题;列出的是使用前需要知道的行为要点。
Python(dpe-sdk)
- 增量推送器
StatefulPusher/AsyncStatefulPusher(#23 系列,PR #125 / #129 / #136 / #142):本地同步状态与意图日志只作缓存,冲突如实上报、绝不自动 force。行为要点: - 判定前的批量读被确定拒绝时逐 URI 定位,只有读被拒的条目返回
failed,其余照常写入;逐条读都通过时全部照常写入,并在 loggerdpe_sdk.pusher上记一条 warning(#145,PR #195); - move 的目标已持有与源相同的内容时不发 move,以 base 删除源并判
moved(#165,PR #179)。 - 媒体来源(PR #120):路径、流与 connector
read_blob回调 → blob 来源。 dpe-run独立运行器(#52 / #53,PR #139 / #153):轮报告、NDJSON 日志、退出码(0 / 1 / 2 / 3 / 4 / 64 / 70 / 74)、删除操作;附零依赖的示例插件examples/markdown-dir(PR #182)与 connector 线协议夹具(PR #190)。
Rust(dpe-sdk)
- HTTP 传输适配与
CredentialProvider(#22,PR #128):remote URL 按 HTTP 绑定 §1 的文法校验(#112);代理只从环境变量读取(#132)。 - 写操作与暂存路径:0.3.1 的 Rust 协议核心只有能力协商与只读操作;快路径 commit、CAS、delete 与 move(PR #118)以及暂存路径
deliver(PR #121)在 0.4 首次提供。 - 增量推送器与媒体来源(PR #135 / #141 / #148 / #149):blob 回调
BlobProvider须为Send,整个操作的 future 因此可交给tokio::spawn(#131,PR #158);推送器的行为要点同 Python。推送器的诊断日志经log门面输出(targetdpe_sdk::pusher),新增依赖log0.4(随 featurehttp)。 - 对 Python 参考服务端的集成(PR #151)。
两侧 dpe-hash
check_remote_url/RemoteUrlError(#181,PR #185)与retry_after_seconds(Rust 另有retry_after_seconds_at,#109,PR #189):SDK 与跑分器共用的判定。RemoteUrlError是调用方的配置错误,不属于DpeHashError/ErrorKind。
跑分器(dpe-conformance,#40,PR #138)
- 首次发布到 PyPI,只依赖
dpe-hash;含错误注入钩子(PR #126)。按 在 CI 中运行跑分器 接入,建议精确锁定dpe-conformance==0.4.0。 - 行为要点:
REMOTE-URL与 SDK 同一文法判定,不合法以退出码 2 拒绝、不开跑(#181);只从环境变量取代理,按每个请求的目的地判定,服务端钩子同样适用(#194,PR #198)。
相对 0.3.1 的变化¶
服务端实现者¶
| 变更 | 以前(规范 0.3.1) | 现在 | 要做什么 |
|---|---|---|---|
| 暂存会话过期时间(#127,PR #152 / #164) | 已描述 negotiate、upload 与断点查询给出过期时间,但不是 MUST;过期边界未定义 | negotiate 与 upload 的各成功响应 MUST 带过期时间(RFC 3339 UTC,以 Z 结尾),断点查询同样 MUST 带 DPE-Session-Expires(给出当前过期时间,不续期);now ≥ expires_at 即过期,且 MUST 以对外给出的那个值判定 |
补齐过期时间(含断点查询);内部判定与对外值保持一致(例如对外值向上取整到秒) |
| 缺失清单(#127) | 页对象 upload 的缺失清单总是完整返回;commit 的 DPE_MISSING_CONTENT 只写了「MAY 截断并注明」 |
页对象 upload 响应 MUST 带 missing_truncated;只有分块上传的大页、且未截断的响应体按 JCS 序列化超过 max_payload_bytes 时,MAY 截断为非空前缀;元素对象的响应不带该字段。commit 的 DPE_MISSING_CONTENT 截断时三层合计 MUST 是实际缺失的非空子集,未截断时 MUST 与实际缺失一致 |
页对象响应加 missing_truncated;截断时保证非空。0.3.1 的 Python 客户端每个补救轮都消耗 max_resends,大页在截断下可能提前失败——宜先让客户端升到 0.4 再开启截断 |
| 带码的 429 / 503(#130,PR #167) | 未限定 | 带 DPE 错误码的 429 / 503 只能在请求未产生任何效果时返回;效果产生之后的限流或不可用改用无码 5xx | 核对限流点:在入口处限流的常见做法天然满足 |
Retry-After(#109,PR #189) |
无提示值时给 1 |
1 只用于预期很快恢复、但给不出具体时间的情况;预计无法在可知时间内恢复时 SHOULD 给足够大的 delay-seconds(如 86400),MUST NOT 用 1 表达无限期。客户端只按 RFC 9110 文法解析,不合文法视同没有提示 |
无限期不可用时给大值;确保 Retry-After 符合 RFC 9110 |
| remote 地址的文法(#112,PR #180) | 只要求 remote 自身路径不带尾部 / |
HTTP 绑定 §1 新增 MUST 级文法:规范化不动点(scheme 与 host 小写、IDN 写成 A-label)、无查询串与片段、无 userinfo、不写默认端口、路径不以 / 结尾且无点段、IPv6 为 RFC 5952 文本、数字形式的主机为严格四段 IPv4;不合法时 0.4 的 SDK、运行器与跑分器在发出请求之前就拒绝 |
核对发给用户的 remote 地址 |
| 再次 negotiate(#171,PR #174) | 未写明 | 服务端可复用可用的会话,客户端不得依赖任一行为 | 兼容澄清,通常无需改动 |
| 演练契约改名(#183,PR #186) | 演练契约名 dpe2 |
改名 dpex,永不分配给真实契约;生产服务端 SHOULD NOT 声明 dpex。upgrade_drill.json 中演练契约的值与 invalid_objects.json 的两处随之变化,dpe1 的值不变 |
消费向量的测试改用新文件;按旧文档声明 dpe2 做演练的环境改为 dpex |
| 新增向量 | — | remote_url.json(kind remote_url,HTTP 绑定 §1)与 retry_after.json(kind retry_after,HTTP 绑定 §5),manifest.json 随之变化;客户端与跑分器 MUST 逐例通过 |
消费向量或移植 SDK 时接入两类新向量;按 manifest 校验文件清单的测试随之更新 |
| 单值响应头(#205,PR #210) | 未写明重复时如何处理 | DPE-Error-Code 与带码 429 / 503 的 Retry-After MUST 恰好一个;客户端按 RFC 9110 §5.3 合并同名字段后再解析,重复时视同没有(重复的错误码没有 problem 体时按非协议错误上报,Retry-After 视同没有提示) |
核对中间层与框架不会重复追加这两个头 |
SDK 使用者¶
| 变更 | 以前(0.3.1) | 现在 | 要做什么 |
|---|---|---|---|
Python:DpeClient 的 remote(#112,PR #180) |
去掉尾部 / 后使用;靠 httpx 解析校验 |
按 HTTP 绑定 §1 的统一文法校验,合法写法唯一:须为 core §1.1 规范化不动点,scheme 为 http / https,无查询串与片段,无 userinfo,不写默认端口,路径不以 / 结尾且无点段,IPv6 须为 RFC 5952 文本,数字形式的主机须为严格四段 IPv4;不合法时构造即报 ValueError |
把 remote URL 改成规范写法(尤其是尾部 /、大写 scheme / host、:443) |
两侧:实例定义的 remote.url(#112) |
只要求 http(s)、有 host、无 userinfo(Python 经 urlsplit,Rust 做结构切分) | 与上一行同一文法,不合法即 definition_invalid |
改正实例定义 |
Python:Retry-After 解析(#109,PR #189) |
经 email.utils 宽松解析,畸形时区会漏出 OverflowError,rfc850 的两位年份按 POSIX 规则解释 |
只按 RFC 9110 文法解析(函数移进 dpe-hash,dpe_sdk.protocol 原路径重新导出);不合文法视同没有提示;首尾只去 SP / HTAB;rfc850 两位年份按 50 年规则解释;任何输入都不抛异常 |
依赖宽松解析的调用方改为处理 None |
Rust:Retry-After 解析(#109) |
已按 RFC 9110 解析,首尾去掉全部 Unicode 空白,rfc850 两位年份固定按当前世纪读 | 首尾只去 SP / HTAB;两位年份取「不晚于当前时刻加 50 年」的最晚年份(世纪交替前后也正确);极端的当前时刻不再整数溢出 | 通常无需改动 |
| Python:代理(#132,PR #193) | httpx 缺省:macOS / Windows 上环境变量为空时会悄悄走系统代理 | 只从 HTTP_PROXY / HTTPS_PROXY / NO_PROXY 等环境变量取代理;代理 URL 有统一文法,密码里的 @ 须写成 %40,socks5:// 等不受支持的写法在构造客户端时报错;隧道被拒抛出的 httpx.ProxyError 判为「确定未发出」 |
依赖系统代理的环境改设环境变量 |
Python:deliver 的补救轮预算(#127,PR #164) |
commit 的每一轮补救都消耗 max_resends |
补救轮只有在没有进展时才消耗 max_resends(缺省 2 不变);max_resends 作为写操作在响应丢失后原样重发的上限,含义不变。wire 模型新增 missing_truncated(缺省 false,对老服务端兼容) |
自定义过该值的调用方按新口径复核 |
| 两侧:演练契约常量(#183) | DRILL_CONTRACT == "dpe2" |
DRILL_CONTRACT == "dpex" |
硬编码过 dpe2 的测试改用常量 |
| 两侧:清单与实例配置的校验消息(#114 / PR #154,#156 / PR #199) | 失败消息会带出用户填写的值(Python 实例配置直接拼接 jsonschema 的英文消息,Rust 回显未通过的串、数值、长度等) | 两侧口径统一为只报位置与约束(关键字及其参数),不回显输入值;Python 在 anyOf / oneOf 失败时报顶层错误,不再钻进分支 | 按消息内容匹配的测试需调整 |
| Python 参考服务端:无限期不可用(#109) | Retry-After: 1 |
Retry-After: 86400 |
断言过该值的测试需调整 |
| 两侧:同名响应头(#205,PR #210) | Python(httpx)按 RFC 9110 合并;Rust 只取最后一个 | 两侧都按 RFC 9110 §5.3 依出现顺序以 , 合并后再解析:Rust 不再只取最后一个;重复的 DPE-Error-Code(不论各项是否相同)两侧都不再挑选 code——有 problem 体按体内 code,否则为非协议错误;重复的 Retry-After 视同没有提示。Python 自写传输适配时,传给核心 Response 的 headers 须已合并 |
通常无需改动 |
两侧:deliver 更新较大的文档(#212) |
整篇内联不超过 max_payload_bytes(缺省 8 MiB)就走快路径,更新时把全部页与元素重传 |
新建照旧走快路径;更新只有整篇不超过 SMALL_UPDATE_BYTES(64 KiB,两侧同值)才走快路径,否则页与元素都只传缺失:negotiate 只交文档对象得到缺失的页,再 negotiate 一次只附带这些页、得到缺失的元素,commit 引用会话并只内联缺失的元素(不缺 blob 时至多三次往返,与新页数量无关);新元素引用的缺失 blob 由 commit 的缺失清单给出、补传后重提 |
通常无需改动;按请求数统计流量的监控会看到更新多出一到两次 negotiate |
connector 规范(自行实现运行器或插件者)¶
以下是 connector 契约相对文档 0.3.1 的修订。使用 0.4 的 dpe-run 时这些行为已经内置;自行实现运行器或插件时需要对照。
| 变更 | 现在的规定 |
|---|---|
| 运行器本地状态故障(#137,PR #143) | 新增运行器自有码 state_unavailable(退出码 2)与 remote_protocol_error;任何一次状态持久化失败,本轮都以 state_unavailable 失败;运行器 MUST NOT 跟随重定向 |
| 实例级校验与登记的顺序(#137,§5.2) | 先取锁,再做本地可判定的实例级校验(清单、配置、凭证引用可解析、必填凭证存在),通过后、发出任何网络请求之前完成前缀登记;remote_unauthorized、remote_incompatible、initialize 的 -32004 / -32005、插件身份不符等须启动插件或访问 remote 才能判定的失败晚于登记,属于已登记实例的失败(例如凭证配错的新实例也会留下登记);取锁时的 I/O 错误归 state_unavailable |
| 删除操作与内部缺陷(#147,PR #150) | 独立运行器的删除操作只以退出码与日志表达;新增 runner_internal_error(退出码 70);item.failed 的失败元素嵌套在 failure 下 |
| 计数、退出码与保存口径的澄清(#155,PR #159 / #160) | 持久化失败轮的计数口径、多种失败并存时退出码的取法、游标未变也算已保存等措辞澄清,属兼容澄清 |
| 写请求的意图按最终结局判定(#130,PR #167) | §7.2:写请求以服务端明确裁决告终时结果确定、清除意图;任何一次尝试结果未知则保留意图 |
remote.url 文法(#112) |
符合 HTTP 绑定 §1 的文法,前缀登记按字节比较 remote;remote.url 属于实例内容身份,改正后下一轮会丢弃游标与同步状态,以全量轮重来 |
| move 的目标已持有同一内容(#165,PR #179) | 不发 move,以 base 为前置条件删除源并判 moved;目标内容不同时仍按「目标已被占用」判条目失败 |
| move 未达成之后的目标写入(#178,PR #196) | 插件产出 move 后 SHOULD 在同一轮紧跟目标的完整 document。本轮发出、尚未得到 moved 的 move 使其目标 URI 未达成,之后涉及未达成 URI 的 document / delete / move 一律不发写、不另计失败;被跳过的写入使它涉及的每个 URI 都未达成。唯一例外:目标上只有一条判定期得出「源已不存在,目标未达成」的 move 时,目标 document 以 if_absent 提交——建出或已等于目标计 moved,被拒且目标确实存在则计「目标已被占用」,绝不覆盖(适用条件与竞态处理的细节见 connector §7.2) |
| 游标未推进时已生效的写入(#213) | §6.8「写入集合与刷新」:运行器 MUST 持久化写入集合 W(自已缓存游标以来意图或同步值变化过的 URI 及其首次变化前的同步值 B),与同步状态、意图日志原子写入,只在持久化了非 null 的新游标时清空。推进任何游标之前对账:增量轮里 W 中有未产出的 URI(W′)时,同一次调用里补一次全量扫描,只落地 W′、其余产出忽略,游标仍取增量扫描的;全量轮直接用本轮产出对账。完整枚举中未产出的 W 成员按「推断删除」处理:只删本实例确认过的值:有 L 时与显式 delete 同等对待(冲突遵从 conflict_policy.delete);没有 L 而 remote 有他人内容时不发写、记一次 DPE_PRECONDITION_FAILED 并移出写入集合。§6.4 写明一次调用可依次发起多个轮首 scan(同一进程内的多轮本已是 §6.7 设想的模型,插件无需改动)。新增 warn 事件 round.refreshed(violations 只供观测);报告 schema 不变。修复了「写入生效、游标未推进,源随后回到原状」时 remote 永久停在错误状态而轮报告 succeeded 的静默分歧 |
| 批量读被拒后的处置(#145,PR #195) | 逐条重读定位:读被拒的条目各记失败,其余按逐条读到的值继续;逐条重读都通过时全部条目同样继续,批量读被拒 SHOULD 以 warn 记入日志(带原错误码) |
仓库内的 Git connector(#170,PR #192;#178,PR #196)¶
- 游标格式版本 2:远端上限变化的那一轮仍是增量轮,删除与改名照常表达;升级前的 v1 游标在上限未变时照样解释。
initialize检查 git ≥ 2.32,过低或 git 起不来以-32005拒绝。- move 的目标总是产出完整 document:每次改名都会完整映射一次目标仓库。
兼容改进(无需迁移)¶
| 变更 | 内容 |
|---|---|
| 分块计算 blob 引用(#146,PR #201) | 两侧 dpe-hash 新增 BlobHasher(update / length / finish() -> "sha256:<hex>"),blob_ref 基于它实现;SDK 的媒体辅助与运行器不再自行拼接 sha256: 引用。Rust dpe-sdk 不再直接依赖 sha2 |
| Rust 的 hash 函数支持契约集合(#157,PR #202) | 新增 page_hash_with / doc_hash_with / object_hash_with / children_with,可传入服务端声明的契约集合,契约升级期的判定与 Python 的 contracts 参数一致;原函数行为不变 |
| 运行器跳过写入的可观测性(#197,PR #203) | connector §7.5 新增 item.skipped(warn)事件:因本轮某条 move 未达成而跳过的写入,以 cause 带上导致跳过的那条 move(跳过链一律追溯到最初的 move);跳过不是失败,不计入 counts 与 failures;§7.2 的对应段落整理为小表,语义不变 |
| 游标约定的共享用例(#191,PR #200) | connector §8.4 新增 cursor_violation 场景(插件声明不支持游标却给出游标 → protocol_error、退出码 2、不缓存游标);§7.1 写明「结束批违反游标约定」归协议级,实现原已如此 |
内核(依赖 dpe-hash)¶
hash 值不变,只需把依赖范围放宽到 dpe-hash>=0.4,<0.5。dpe-hash 新增的 API(check_remote_url、retry_after_seconds、BlobHasher,Rust 另有 *_with 系列)不影响现有调用;若测试里硬编码过演练契约名 dpe2,改用 DRILL_CONTRACT(现为 dpex)。