☰
Apache OpenDAL Deleter API 深度解析:RFC 3911 如何统一批量与递归删除设计
2026/10/14 15:37:09 网站建设 项目流程
  • 数据存储
  • 后端

【免费下载链接】opendal

Apache OpenDAL: One Layer, All Storage.

项目地址:https://gitcode.com/gh_mirrors/op/opendal
点击查看免费下载

本文基于 OpenDAL 仓库中的 RFC 文档 3911_deleter_api.md,讲解DeleterAPI 的设计动机、提案内容,以及它在当前源码中的落地形态:从Operator::deleter()的批量删除流式接口,到底层oio::Delete抽象与OneShotDeleter/BatchDeleter两种实现路径。读完后,你将掌握如何用Deleter完成单文件删除、流式批量删除、基于Sink的转发删除,以及如何用delete_with(...).recursive(true)替代旧有的remove_all递归删除。

1. 背景:删除 API 与其余公共 API 的设计不一致

RFC 的 Motivation 部分指出了 OpenDAL 删除接口的历史包袱。OpenDAL 的所有公共 API 都遵循同一套命名约定,以读取为例:

  • read:执行一次读操作;
  • read_with:带额外选项(如 range、if_match)的读操作;
  • reader:创建一个流式 Reader,支持灵活的数据访问;
  • reader_with:带高级选项的 Reader 创建。

但删除操作打破了这套约定。历史上 OpenDAL 提供了多个语义各异的删除方法:

  • delete:删除单个文件或空目录;
  • remove:删除一组文件;
  • remove_via:删除一个流式产生的文件列表;
  • remove_all:删除某路径下的全部文件。

这套设计既与其他 API 不一致,使用上也不够直观。RFC 因此提出引入Deleter,把“单文件删除、批量删除、流式删除、递归删除”统一收敛到一个类型上。

2. RFC 提案的公共 API 形态

RFC 的 Guide-level explanation 部分给出了在Operator上拟新增的接口(原文以 diff 形式呈现):

impl Operator { pub async fn delete(&self, path: &str) -> Result<()>; + pub fn delete_with(&self, path: &str) -> FutureDelete; + pub async fn deleter(&self) -> Result<Deleter>; + pub fn deleter_with(&self) -> FutureDeleter; }

四个 API 的职责划分是:

  • delete:既有 API,删除单个文件或空目录;
  • delete_with:delete的扩展,支持version等附加选项;
  • deleter:新 API,返回一个可复用的Deleter实例;
  • deleter_with:deleter的扩展,支持concurrent等附加选项。

RFC 中同时定义了两个面向delete_with/deleter_with的选项:

  • concurrent:允许多少个删除任务并发执行;
  • buffer:单个批次中最多缓冲多少个待删除文件。

2.1 基本用法:delete / delete_all / close 三步

RFC 给出的标准用法是先初始化Deleter,再逐个或流式地投递路径,最后close保证所有输入都被删除:

let deleter = op.deleter().await?; // Add a single file to the deleter. deleter.delete(path).await?; // Add a stream of files to the deleter. deleter.delete_all(&mut lister).await?; // Close deleter, make sure all input files are deleted. deleter.close().await?;

这里的关键语义是:delete返回成功只表示请求已被Deleter接受,底层实现可能是立即执行,也可能是先缓冲、攒批后再执行;而close负责刷出缓冲并等待所有已接受的删除完成。这一点在当前源码的 trait 文档中依然被明确强调(见下文第 4 节)。

2.2 作为 Sink 使用:forward / send_all

RFC 还提出Deleter应实现futures的Sinktrait,使得任何Stream都能直接转发进Deleter:

// Init a deleter to start batch delete tasks. let deleter = op.deleter().await?; // List all files that ends with tmp let lister = op.lister(path).await? .filter(|x| future::ready(x.ends_with(".tmp"))); // Forward all paths into deleter. lister.forward(deleter).await?; // Send all from a stream into deleter. deleter.send_all(&mut lister).await?; // Close the deleter. deleter.close().await?;

这一设计的实战价值在于:把lister(列举)与deleter(删除)两个流式组件直接串联,实现“边列举边删除”的管道,无需在内存中先物化完整路径列表。

2.3 通过 deleter_with 控制行为

RFC 同时展示了通过选项对象调节Deleter行为的示例:

let deleter = op.deleter_with() // Allow up to 8 concurrent delete tasks, default to 1. .concurrent(8) // Configure the buffer size to 1000, default value provided by services. .buffer(1000) .await?; // Add a single file to the deleter. deleter.delete(path).await?; // Add a stream of files to the deleter. deleter.delete_all(&mut lister).await?; // Close deleter, make sure all input files are deleted. deleter.close().await?;

其中concurrent默认为 1,buffer的默认值由各存储服务自身提供。

2.4 旧 API 的收敛

RFC 明确了旧 API 的去向:随着Deleter落地,remove、remove_via、remove_all将被移除——

  • remove与remove_via可直接由Deleter替代;
  • remove_all可由delete_with(path).recursive(true)替代。

这一承诺在当前仓库中已有体现:Operator::remove_all被标注为自 v0.55.0 起弃用,函数体直接转发给新 API,见 operator.rs:

#[deprecated( since = "0.55.0", note = "Use `delete_with` with `recursive(true)` instead" )] pub async fn remove_all(&self, path: &str) -> Result<()> { self.delete_with(path).recursive(true).await }

对应地,FutureDelete提供了recursive方法,见 operator_futures.rs。

3. Reference-level 设计:Accessor 关联类型与 batch API 的移除

RFC 的 Reference-level explanation 给出了底层改造方案:在Accessortrait 上新增关联类型,让删除能力与读写能力一样成为服务适配层的一等公民:

trait Accessor { ... type Deleter = oio::Delete; type BlockingDeleter = oio::BlockingDelete; }

同时,delete操作签名被改为接收参数对象并返回 deleter:

trait Accessor { - async fn delete(&self) -> Result<(RpDelete, Self::Deleter)>; + async fn delete(&self, args: OpDelete) -> Result<(RpDelete, Self::Deleter)>; }

伴随这一变更,RFC 宣布移除Accessor上独立的batch删除接口:

trait Accessor { - async fn batch(&self, args: OpBatch) -> Result<RpBatch>; }

也就是说,批量删除不再是一个平行的独立操作,而是被统一收编进Deleter抽象——服务若支持批量,就通过批量 deleter 表达;不支持,就退化为逐条删除。当前仓库的演进正是沿这条路线展开的:旧的Accessor已被更细粒化的服务架构取代,但“删除统一经由oio::Delete抽象”的核心思想完整保留,见下一节。

4. 当前源码中的公开实现:types::delete::Deleter

当前仓库中,Deleter的公开实现在 core/core/src/types/delete/deleter.rs:

pub struct Deleter { deleter: oio::Deleter, scheme: &'static str, capability: Capability, }

它持有类型擦除后的底层oio::Deleter、服务 scheme 与能力描述(Capability)。相比 RFC 草案,公开方法面有所扩充,最终形成了四个“投递”入口 +close+into_sink的完整结构(deleter.rs):

  • delete(impl IntoDeleteInput):删除单个条目。可以直接传路径字符串;也可以传(String, DeleteOptions)元组,为这一条删除单独指定if_not_changed、if_match等条件选项;
  • delete_iter/delete_try_iter:消费不可失败 / 可失败的IntoIterator路径集合——这正是 RFC “Future possibilities” 一节设想“接受IntoIterator的新 API”的最终落地形态;
  • delete_stream/delete_try_stream:消费不可失败 / 可失败的Stream,是 RFC 中delete_all(&mut lister)的对应物;
  • close():刷出缓冲并等待所有已接受的删除完成;
  • into_sink():把Deleter转换为FuturesDeleteSink,实现 RFC 中“作为 Sink 使用”的能力,见 futures_delete_sink.rs。

IntoDeleteInput的输入转换定义在 core/core/src/types/delete/input.rs。条件选项的处理路径是:Deleter::delete先将输入归一化为(path, DeleteOptions),对路径执行normalize_path,再通过OpDelete::from_options(&self.capability, options)依据服务能力把选项“降维”为服务可直接消费的参数(deleter.rs)。

此外,Operator上还有一组便捷方法,内部等价于“创建Deleter→ 投递 →close”的完整闭环,例如 operator.rs 中的delete_iter、delete_try_iter、delete_stream、delete_try_stream:

pub async fn delete_iter<I, D>(&self, iter: I) -> Result<()> where I: IntoIterator<Item = D>, D: IntoDeleteInput, { let mut deleter = self.deleter().await?; deleter.delete_iter(iter).await?; deleter.close().await?; Ok(()) }

创建入口Operator::deleter()定义在 operator.rs:

pub async fn deleter(&self) -> Result<Deleter> { Deleter::create(self.context().clone(), self.service().clone()) }

与 RFC 草案的deleter_with() -> FutureDeleter对应,当前仓库保留了 FutureDeleter 类型别名(OperatorFuture<OpDeleter, (), F>)。值得注意的是,从源码结构看,raw/ops.rs 中的OpDeleter当前是一个空参数结构——可以推断,RFC 时期的concurrent/buffer选项在后续架构演进中不再由用户侧 future 显式配置,而是由服务能力(capability)与服务实现自行决定批量大小,用户面 API 因此简化了。

5. 底层实现:oio::Delete 抽象与两种适配器

RFC 中“Accessor::Deleter = oio::Delete”的设计在当前源码中演化为独立的删除原语模块 core/core/src/raw/oio/delete/。核心 trait 定义在 api.rs:

/// Deleter is a type erased [`Delete`]. pub type Deleter = Box<dyn DeleteDyn>; pub trait Delete: Unpin + Send + Sync { fn delete<'a>( &'a mut self, path: &'a str, args: OpDelete, ) -> impl Future<Output = Result<()>> + MaybeSend + 'a; fn close(&mut self) -> impl Future<Output = Result<()>> + MaybeSend; }

trait 文档明确了两点契约:实现可以立即执行每个请求,也可以缓冲请求以进行批量删除;调用方必须在丢弃 deleter 前调用close刷出挂起的工作。Deleter类型即Box<dyn DeleteDyn>——类型擦除使得任何服务实现都能以统一方式挂到公开types::delete::Deleter之下。

该抽象下有两个官方适配器:

5.1 OneShotDeleter:逐条即时删除

one_shot_delete.rs 定义OneShotDeletetrait(仅需实现delete_once)与OneShotDeleter包装器。它对Delete的实现非常直接:delete调用立即转发给delete_once,close为空操作。适用于不支持批量删除的服务——每个删除请求独立、同步完成,无缓冲语义。

5.2 BatchDeleter:攒批 + 结果核对

batch_delete.rs 面向支持批量删除的服务。BatchDeletetrait 要求实现方提供:

  • delete_once(path, args):删除单个路径;
  • delete_batch(batch: Vec<(String, OpDelete)>):一次删除多个路径,返回BatchDeleteResult,其中包含succeeded与failed两个列表(成功/失败项均带原路径与参数,失败项额外携带错误)。

BatchDeleter内部用HashSet<(String, OpDelete)>作缓冲,配合max_batch_size控制单批上限,flush_buffer的关键逻辑体现了防御式设计(batch_delete.rs):

  • 缓冲中只有 1 个路径时,退化走delete_once;
  • 批量结果若全部失败(succeeded为空)则报Unexpected错误;
  • 批量结果的succeeded + failed总数必须与发送数量一致,否则视为结果不匹配并报错;
  • 只有成功项从缓冲中移除,失败项保留以便后续重试或暴露错误。

这套“缓冲—刷出—核对”机制正是 RFC 中buffer选项意图的最终承载:单条delete的返回只代表“请求被接受”,真正的批量执行发生在缓冲达到上限或close被调用时。

6. 条件删除与递归删除

6.1 逐条条件选项

RFC 提到delete_with支持version等选项,当前实现将这一能力细化为每条删除可携带独立的DeleteOptions。行为测试 async_delete.rs 中的test_deleter_if_not_changed展示了用法:

let mut deleter = op.deleter().await?; deleter .delete(( path.clone(), options::DeleteOptions { if_not_changed: Some(expected), ..Default::default() }, )) .await?; deleter.close().await?;

底层参数结构OpDelete(raw/ops.rs)内部以位标记recursive与Version/IfMatch/IfNoneMatch/IfVersionMatch/IfVersionNotMatch等条件字段,from_options会依据服务能力对不支持的选项做降级处理,并保证路径已归一化。

6.2 递归删除:delete_with(path).recursive(true)

RFC 承诺“remove_all由delete_with(path).recursive(true)替代”,当前源码中FutureDelete提供 recursive 方法,OpDelete通过set_recursive位标记传递该语义给服务层(raw/ops.rs)。配合已弃用的remove_all(见第 2.4 节),旧代码的迁移路径就是文档注释中给出的:

// 旧 // op.remove_all("path/to/dir").await?; // 新 op.delete_with("path/to/dir").recursive(true).await?;

7. 测试依据

Deleter的行为有两层测试覆盖,可作为引用证据:

  • 单元测试在 deleter.rs 末尾的mod tests中:test_sink_close_must_flush_buffered_deletes用模拟批量 deleter 验证into_sink()后close必定刷出缓冲中的全部删除;test_delete_normalizes_path验证传入带前导/的路径也会被正确归一化并删除成功;
  • 行为测试在 core/tests/behavior/async_delete.rs 中:test_deleter先写入 100 个文件,再用op.deleter()+delete_iter+close全部删除并逐一校验exists为 false;test_deleter_if_not_changed验证条件删除选项的透传(见第 6.1 节)。

8. 旧 API 迁移对照

结合 RFC 承诺与当前仓库状态,删除相关 API 的迁移关系可整理为:

旧 API状态新用法
delete保留op.delete(path),单文件/空目录删除
remove(批量列表)RFC 承诺移除op.deleter()后逐条delete或delete_iter,最后close
remove_via(流式列表)RFC 承诺移除deleter.delete_stream(&mut lister)+close,或into_sink()后forward
remove_all(递归)自 v0.55.0 弃用op.delete_with(path).recursive(true)

9. 小结

RFC 3911 的核心贡献是把 OpenDAL 中语义割裂的四个删除方法收敛为一套与read/reader对称的delete/deleter体系:Deleter以“投递 + close”的流式语义统一了单条、迭代器、流式与递归删除场景,底层则通过oio::Delete抽象让服务自行选择OneShotDeleter(逐条执行)或BatchDeleter(攒批执行并核对结果)两条路径。当前仓库的公开实现不仅完整兑现了 RFC 的 API 面(含 RFC “Future possibilities” 中设想的IntoIterator入口),还通过IntoDeleteInput支持逐条携带条件选项,并用单元测试与行为测试锁定了缓冲刷出、路径归一化、条件透传等关键语义。对于需要清理大批量对象的场景,推荐模式是:op.lister(path)产出路径流,op.deleter()接收并删除,最后close等待刷出——这正是 RFC 设计所指向的“列举即删除”管道。

  • 数据存储
  • 后端

【免费下载链接】opendal

Apache OpenDAL: One Layer, All Storage.

项目地址:https://gitcode.com/gh_mirrors/op/opendal
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询