OpenDAL Lister API:从 RFC 2774 到 list/lister 分层设计的实现剖析

发布时间:2026/10/14 15:24:33
OpenDAL Lister API:从 RFC 2774 到 list/lister 分层设计的实现剖析 数据存储后端【免费下载链接】opendalApache OpenDAL: One Layer, All Storage.项目地址https://gitcode.com/gh_mirrors/op/opendal点击查看免费下载本篇围绕 Apache OpenDAL 的 RFC《lister_api》提案 2774_lister_api.md展开先梳理该提案要解决的 API 一致性问题与设计决策再结合当前仓库源码讲清Operator::list与Operator::lister两套接口的完整用法、Lister的Stream实现原理以及分页、递归等参数如何从ListOptions一路传递到底层服务实现。读完后你将掌握在 OpenDAL 中按需选择“一次性取全部条目”或“流式惰性遍历”的正确姿势并理解其底层类型擦除机制。提案背景API 命名不一致带来的困惑RFC 的动机非常直接。在提案提出之前OpenDAL 中获取列表条目的写法是let lister op.list().await?;也就是说list()返回的其实是一个“列表器”lister用户需要对其逐条迭代。这与 OpenDAL 中read/reader的命名约定不一致read是一次性取出数据reader才是按需拉取的流。list却承担了reader的语义容易让使用者混淆。因此提案给出的目标是与read/reader等其他 OpenDAL API 保持命名与语义对齐简化常用场景的使用方式——大多数用户只是想“列出这个前缀下的所有条目”而不是自己管理迭代过程。提案确定的新 API 形态为let entries op.list().await?; // 直接拿到所有条目 let lister op.lister().await?; // 拿到 lister按需逐条获取参考级设计三步改造方案RFC 的“Reference-level explanation”部分给出了明确的落地路线将原有的list重命名为lister新增list方法内部调用lister并一次性返回全部条目将scan合并进list_with以delimiter()的方式表达全量前缀扫描。这一设计的关键约束是分页逻辑必须封装在lister内部。用户无论通过list还是lister使用都不需要关心底层对象存储的分页游标continuation token、start_after等细节。当前仓库的实现与提案完全吻合。在 operator.rs 中可以确认这一完整 API 族方法返回类型用途Operator::list(path)ResultVecEntry一次性取回前缀下所有条目Operator::list_with(path)链式 future.recursive(true)等带选项的批量列举Operator::list_options(path, opts)ResultVecEntry显式传入ListOptions的批量列举Operator::lister(path)ResultLister创建流式列表器Operator::lister_with(path)链式 future带选项创建列表器Operator::lister_options(path, opts)ResultLister显式传入ListOptions创建列表器前缀语义list 与 lister 共用的行为契约无论选择哪个入口前缀prefix语义都是一致的源码中的文档注释给出了明确契约见 operator.rs列举是基于前缀的不要求父目录真实存在如果path自身存在文件或目录它本身也会作为一个条目被返回如果path不存在但存在更深层的对象如path/child列举依然成功返回这些带前缀的条目而不是报错。此外文档注释中反复强调了一个重要提示list会把整个列表物化materialize到内存对于大规模列举场景应优先使用lister进行流式遍历。这一取舍建议正是提案“简化常用场景、保留流式能力”意图的直接体现。ListOptions列举选项的完整说明两套 API 共享同一组选项定义在 options.rs/// Options for list operations. /// /// # Groups /// - Traversal: recursive. /// - Pagination: limit, start_after. /// - Versioning: versions, deleted (effective on version-aware backends). #[derive(Debug, Clone, Default, Eq, PartialEq)] pub struct ListOptions { /// Maximum results per request (backend hint) to control memory and throttling. pub limit: Optionusize, /// The start_after passes to underlying service to specify the specified key /// to start listing from. pub start_after: OptionString, /// Whether to list recursively under the prefix; default false. pub recursive: bool, /// Include object versions when supported by the backend; default false. pub versions: bool, /// Include delete markers when supported by version-aware backends; default false. pub deleted: bool, }各字段含义与默认值如下limit默认None每次请求返回的最大条目数属于“后端提示”用于控制单次列举的内存占用与限流start_after默认None指定从哪个 key 之后开始列举可视为续传/断点位置具体支持程度依赖后端recursive默认false是否在前缀下递归列举即“深度优先”遍历所有子层versions默认false在支持版本化的后端上返回对象的历史版本条目deleted默认false在支持版本化的后端上额外返回 delete marker 条目。注意versions与deleted的注释明确写明“在版本感知后端version-aware backends才生效”——这是可移植性portability层面的约束不同存储服务的能力不同使用前应结合Operator::info().capability()判断。这些选项如何传递到内部在 ops.rs 中ListOptions会被转换opts.into()为内部操作参数OpList/// Arguments for list operation. #[derive(Debug, Clone, Default)] pub struct OpList { /// The maximum number of results that the service should return per request. limit: usize, flags: u8, values: CompactValues, } const OP_LIST_HAS_LIMIT: u8 1; const OP_LIST_RECURSIVE: u8 1 1; const OP_LIST_VERSIONS: u8 1 2; const OP_LIST_DELETED: u8 1 3;从源码结构看recursive、versions、deleted以及是否携带limit都以位标志bit flags紧凑编码在flags中start_after等字符串值则存放在CompactValues里。这种编码方式让OpList成为一份廉价克隆、便于跨层传递的操作描述。Lister一个实现了 Stream 的惰性列表器提案的核心产出是Lister类型。当前实现位于 list.rs/// Lister lists entries at a given path asynchronously. /// /// - Lister implements StreamItem ResultEntry. /// - Lister returns None after it yields all entries or encounters an error. pub struct Lister { lister: Optionoio::Lister, fut: OptionBoxedStaticFuture(oio::Lister, ResultOptionoio::Entry), errored: bool, }三个字段各司其职lister底层类型擦除后的服务列表器oio::Lister真正执行分页拉取fut正在进行的“取下一条”异步任务。由于Stream::poll_next是无阻塞轮询这里把一个可恢复的 future 挂起来等待跨poll轮次完成errored错误标记。一旦出现错误就置位后续poll_next直接返回Poll::Ready(None)结束流保证错误后流可安全收尾。其Stream实现的关键逻辑在poll_next中impl Stream for Lister { type Item ResultEntry; fn poll_next(mut self: Pinmut Self, cx: mut Context_) - PollOptionSelf::Item { // Returns None if we encountered an error. if self.errored { return Poll::Ready(None); } if let Some(mut lister) self.lister.take() { let fut async move { let res lister.next_dyn().await; (lister, res) }; self.fut Some(Box::pin(fut)); } // ... return match entry { Ok(Some(oe)) Poll::Ready(Some(Ok(oe.into_entry()))), Ok(None) { self.lister None; Poll::Ready(None) } Err(err) { self.errored true; Poll::Ready(Some(Err(err))) } }; } }这里体现了提案所说的“分页逻辑封装在 lister 内”调用方只看到一条StreamItem ResultEntry每次next时由内部列表器决定是复用当前页缓存还是发起新的分页请求。错误语义上遵循TryStream惯例——先产出Err项随后流终止耗尽条目时产出None终止。底层的服务侧接口定义在 api.rs/// List is returned by [Service] to stream entries for a list operation. pub trait List: Unpin Send Sync { /// Fetch the next [Entry]. /// /// Ok(Some(entry)) means one entry is available. /// Ok(None) means the list operation has completed. Further calls must /// keep returning Ok(None). fn next(mut self) - impl FutureOutput ResultOptionEntry MaybeSend; } /// ListDyn is the dyn version of [List]. pub trait ListDyn: Unpin Send Sync { /// The dyn version of [List::next]. fn next_dyn(mut self) - BoxedFuture_, ResultOptionEntry; }List是各服务fs、s3、gcs 等实现分页游标推进的 traitListDyn是其动态分发版本oio::Lister Boxdyn ListDyn由此成为面向Operator层的类型擦除句柄。List文档中对Ok(None)的约束值得注意一旦返回Ok(None)后续调用必须继续返回Ok(None)这是保证流终止行为一致性的底层约定。list 与 lister 的关系共享同一条调用链回到提案“新增list方法调用lister并返回所有条目”的设计当前实现精确地落实了这一点。operator.rs 中两条链路汇合于同一内部函数#[inline] async fn list_inner( ctx: OperationContext, srv: Servicer, path: String, opts: options::ListOptions, ) - ResultVecEntry { let args opts.into(); let lister Lister::create(ctx, srv, path, args)?; lister.try_collect().await } // Keep this async so it can be used as an OperatorFuture factory. #[allow(clippy::unused_async)] #[inline] async fn lister_inner( ctx: OperationContext, srv: Servicer, path: String, opts: options::ListOptions, ) - ResultLister { let args opts.into(); Lister::create(ctx, srv, path, args) }list_inner内部创建Lister后用try_collect()把整个流收集为VecEntrylister_inner则把同一个Lister直接交给用户。也就是说list本质上是listercollect的语法糖两者的行为差异只在于结果是否物化到内存。Lister::create的实现list.rs也非常直接向服务层要一个oio::Lister然后初始化内部状态pub(crate) fn create( ctx: OperationContext, srv: Servicer, path: str, args: OpList, ) - ResultSelf { let lister srv.list(ctx, path, args)?; Ok(Self { lister: Some(lister), fut: None, errored: false, }) }另有两个细节值得留意lister/list系列方法在入口处都会先调用normalize_path(path)归一化前缀lister_with/list_with返回的是FutureLister/FutureList这类链式 future支持.recursive(true).limit(1000).await这种 builder 风格调用选项在 await 前即可逐步叠加。实战两种用法对比场景一小规模前缀一次性取回use opendal_core::EntryMode; use opendal_core::Operator; async fn list_all(op: Operator) - anyhow::Result() { let entries op.list(path/to/dir/).await?; for entry in entries { match entry.metadata().mode() { EntryMode::FILE println!(file: {}, entry.path()), EntryMode::DIR println!(dir: {}, entry.path()), EntryMode::Unknown continue, } } Ok(()) }场景二大规模前缀流式惰性遍历use futures::TryStreamExt; async fn list_stream(op: Operator) - anyhow::Result() { let mut ds op.lister(path/to/dir/).await?; while let Some(entry) ds.try_next().await? { match entry.metadata().mode() { EntryMode::FILE println!(Handling file {}, entry.path()), // 对目录条目可以用 entry.path() 作为前缀开启新一轮列举 EntryMode::DIR println!(Handling dir {}, entry.path()), EntryMode::Unknown continue, } } Ok(()) }以上示例取自 operator.rs 的官方文档注释已做精简。场景三递归 选项控制的流式列举use opendal_core::options; async fn list_recursive(op: Operator) - anyhow::Result() { let mut lister op .lister_options(path/to/dir/, options::ListOptions { recursive: true, limit: Some(1000), // 每页请求上限后端提示 ..Default::default() }) .await?; while let Some(entry) lister.try_next().await? { println!({}, entry.path()); } Ok(()) }也可以采用 builder 风格op.lister_with(path/to/dir/).recursive(true).await?。阻塞式 API 中的 Lister对非 async 运行时用户OpenDAL 提供了阻塞版包装位于 list.rs/// BlockingLister is designed to list entries at given path in a blocking /// manner. /// /// - Lister implements IteratorItem ResultEntry. /// - Lister will return None if there is no more entries or error has been returned. pub struct Lister { handle: tokio::runtime::Handle, lister: OptionAsyncLister, } impl Iterator for Lister { type Item ResultEntry; fn next(mut self) - OptionSelf::Item { let Some(lister) self.lister.as_mut() else { /* lister 已耗尽 */ }; self.handle.block_on(lister.next()) } }其实现思路是把异步Lister包进Iterator每次next()通过handle.block_on驱动一次异步拉取Drop实现中还会block_on释放内部异步列表器确保资源在 tokio 运行时上正确清理。也就是说阻塞 API 与异步 API 共享同一套底层实现仅在外层做了同步桥接——这进一步印证了提案“分页逻辑内聚于 lister”的架构收益。行为测试list/lister 契约如何被验证仓库中的行为测试套件 async_list.rs 系统地覆盖了这两类 API 的语义。测试注册入口在能力检查cap.read cap.write cap.list通过后挂载见 async_list.rs包括test_list_dir/test_list_prefix验证新写入文件能被对应前缀列举命中且元数据EntryMode、content_length正确test_list_rich_dir、test_list_nested_dir、test_list_sub_dir目录层级相关的列举行为test_list_non_exist_dir/test_list_non_exist_dir_with_recursive前缀不存在时不报错的契约test_list_with_start_afterstart_after分页断点选项test_list_dir_with_recursive系列recursive选项test_list_files_with_versions、test_list_files_with_deleted、test_list_with_versions_and_limit版本化选项在支持版本的后端上的行为。其中test_list_dir直接演示了流式用法op.lister(format!({parent}/)).await?后try_next()逐条遍历与上文实战示例一致。测试通过gen_bytes(op.info().capability())根据服务能力生成数据说明列举行为测试对不同后端的差异化能力有明确感知。提案的后续影响lister 成为各语言绑定的一等公民从 CHANGELOG.md 可以追踪到该 API 确立后的持续扩散C 绑定暴露了lister“expose lister”、Ruby 绑定新增lister、Node.js 绑定补齐缺失的 lister 方法、Go 绑定持续为lister.go增加opendal_list_options_set_limit、opendal_list_options_set_start_after、opendal_list_options_set_versions、opendal_list_options_set_deleted等选项函数。这些记录表明RFC #2774 确立的list/lister双层 API 已成为 OpenDAL 各语言绑面的标准面之一ListOptions的四个可移植选项也随之在各绑定中落地。此外 CHANGELOG 中还有两条与 lister 健壮性相关的修复值得留意fix(core): Invalid lister should not panic nor endless loop无效 lister 不 panic 也不死循环与fix(core): skip PermissionDenied errors in recursive lister递归列举时跳过权限拒绝错误——前者对应上文errored标记保证流可安全终止的实现后者说明递归列举在遇到无权限子节点时的容错策略。小结RFC《lister_api》用一次 API 重命名解决了 OpenDAL 列举接口的语义对齐问题list面向“我要全部条目”的常见需求一次性返回VecEntry实现上是lister流的try_collect()lister面向大规模前缀与惰性遍历返回实现StreamItem ResultEntry的Lister把分页、递归、版本化等复杂细节全部内聚在流内部两者共享ListOptionslimit/start_after/recursive/versions/deleted与同一套前缀语义并通过OpList位标志编码传递给各服务实现阻塞 API 以Iterator桥接复用同一异步实现行为测试则对列举契约做了系统性守护。对于使用者选择标准可以概括为一句话结果集可控且较小用list结果集可能很大或需要边遍历边处理用lister。赞分享数据存储后端【免费下载链接】opendalApache OpenDAL: One Layer, All Storage.项目地址https://gitcode.com/gh_mirrors/op/opendal点击查看免费下载相关推荐Apache OpenDAL 目录列表能力演进从 ObjectStream RFC 到现代 Lister APIApache OpenDAL 目录列表能力演进从 ObjectStream RFC 到现代 Lister API 导读 本文以 OpenDAL 核心仓库中的数据存储后端Apache OpenDAL 目录项设计演进从 RFC-0337 DirEntry 到今日 Entry/Lister 的完整解读Apache OpenDAL 目录项设计演进从 RFC 0337 DirEntry 到今日 Entry/Lister 的完整解读 导读 本篇技术指南以 Apa数据存储后端Apache OpenDAL Append API 设计剖析从 RFC 2133 提案到可追加剧写实现Apache OpenDAL Append API 设计剖析从 RFC 2133 提案到可追加剧写实现 本篇技术指南以 Apache OpenDAL 的 ap数据存储后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询