- 版本控制
- 后端
【免费下载链接】lore
Lore is a next-generation, open source version control system
本篇技术指南围绕 Lore 代码标准文档 tasks.md 展开,系统讲解 Lore 这个下一代开源版本控制系统在 Rust 代码库中统一管理异步任务的标准做法:以lore_spawn!宏族作为唯一任务入口、让LORE_CONTEXT跨任务自动传播、用JoinSet与lore_drain_tasks!治理并行任务、以AbortOnDropHandle处理任务取消,并给出按库分类(库代码 / 服务端 / 外部服务 crate)的具体用法。读完本文,你将能依据该标准在 Lore 及其衍生 crate 中正确写出可传播上下文、可观测、可安全关停的异步与阻塞任务代码。
一、为什么 Lore 需要一套任务调度标准
Lore 是一个由 Rust 编写、面向大规模仓库与高并发连接的版本控制系统。服务端同时服务数千个客户端连接,一次命令执行会在多个异步任务间分派工作(例如 push 分片时同时运行进度上报 ticker 与分片传输),这些任务共享着同一个进程级 tokio 运行时。在这一背景下,如果每个模块各自直接调用tokio::spawn,会出现两个现实问题:
- 上下文丢失:Lore 用
LORE_CONTEXT(一个 task-local 的Arc<dyn Any + Send + Sync>)承载执行上下文(如关联 ID、统计级别、事件分发器),子任务一旦脱离调用链就无法关联到发起它的那条命令; - 可观测性缺失:任务的生命周期(启动、完成、被丢弃)无人记录,排障时无法把一次任务的结束对应到它的开始。
因此 tasks.md 规定:整个代码库的所有任务生成必须走lore_spawn!宏族(或下文按 crate 分类的替代形式),以确保LORE_CONTEXT传播与任务生命周期观测。这一约束不仅是文档约定,还通过 clippy 的disallowed-methods机制强制落地:根目录 clippy.toml 中把tokio::spawn、tokio::task::spawn、tokio::task::spawn_blocking、tokio::runtime::Handle::spawn、tokio::task::JoinSet::spawn等全部列入禁用名单,理由是「请改用lore_base::lore_spawn!以让LORE_CONTEXT传播到被生成的任务」。每个 crate 的clippy.toml都逐字复制了这份名单,以避免 clippy 就近解析配置导致的遮蔽。
二、核心宏一览
所有宏都定义在 lore-base/src/runtime.rs(lore_spawn!自第 175 行起),任何任务生成都必须使用它们。
| 宏 | 用途 |
|---|---|
lore_spawn!(task) | 生成异步任务 |
lore_spawn!("name", task) | 生成带名字的异步任务 |
lore_spawn!(joinset, task) | 生成任务并放入JoinSet |
lore_spawn!(joinset, "name", task) | 生成带名字的任务并放入JoinSet |
lore_spawn_blocking!(task) | 生成带上下文传播的阻塞任务 |
lore_spawn_blocking_nocontext!(task) | 生成不传播上下文的阻塞任务 |
lore_spawn_guarded!(task) | 生成必须在运行时关停前完成的任务 |
lore_drain_tasks!(tasks, err) | 排空JoinSet并传播第一个错误 |
lore_limit_drain_tasks!(tasks, max, err) | 有界并发的非阻塞排空 |
所有lore_spawn!变体都会在调用任务设置了LORE_CONTEXT时自动将其传播到新任务;若调用处未设置上下文,则任务不带上下文作用域生成。阻塞变体支持与普通变体完全相同的形式(裸、命名、JoinSet、命名JoinSet)。
除上表外,runtime.rs 还定义了同一语义的另两个运行时定向宏,用于既有运行时划分场景(详见下文「运行时与定向宏」小节):
| 宏 | 用途 |
|---|---|
lore_spawn_net! | 在专用网络运行时上生成任务并传播LORE_CONTEXT(自第 246 行起) |
lore_spawn_core! | 在核心运行时上生成任务并传播LORE_CONTEXT(自第 312 行起) |
三、LORE_CONTEXT:任务本地上下文与传播原理
LORE_CONTEXT在 lore-base/src/runtime.rs 中声明为 tokio task-local:
tokio::task_local! { /// Opaque task-local context propagated by `lore_spawn!`. /// `lore` sets this to `Arc<ExecutionContext>`. Transport and storage /// code propagate it without knowing the concrete type. pub static LORE_CONTEXT: Arc<dyn Any + Send + Sync>; }它被刻意设计为不透明(Arc<dyn Any + Send + Sync>):lore库将其设为Arc<ExecutionContext>,而传输与存储层代码只需传播它、无需知道具体类型,从而避免了层层 crate 之间的类型耦合。配套提供了两个访问函数:
lore_context():读取当前 task-local 上下文,未设置时 panic;try_lore_context():读取上下文,未设置时返回None。
lore_spawn!的展开逻辑正是围绕try_lore_context()展开的——以 runtime.rs 中的裸生成分支为例:
($expression:expr) => {{ #[allow(clippy::disallowed_methods)] { let __task = $crate::runtime::ObservedTask::new($expression); if let Some(__ctx) = $crate::runtime::try_lore_context() { $crate::runtime::runtime().spawn($crate::runtime::LORE_CONTEXT.scope(__ctx, __task)) } else { $crate::runtime::runtime().spawn(__task) } } }};可以看到两个关键设计:
- 传播即
LORE_CONTEXT.scope(__ctx, __task):新任务在scope包裹的 future 内运行,继承生成点的上下文;无上下文时直接生成不包裹的任务。 - 每个生成的 future 都先包一层
ObservedTask::new(...):ObservedTask(runtime.rs)把任务生命周期接入进程级的TaskLifecycleObserver——任务开始、完成、被丢弃都会触发on_event回调,并且每次事件都携带生成时的LoreTaskSpawn(文件、行号、context_label)。这使任务计数能在生成线程与工作线程上下文不一致的情况下正确配对增减,为任务生命周期观测(见 lore-base/tests/task_lifecycle_observer.rs)提供数据基础。
lore_spawn_blocking!的传播方式不同:它使用LORE_CONTEXT.sync_scope(__ctx, $expression)(runtime.rs),因为阻塞闭包不是 async 上下文,需要以同步作用域方式把上下文注入闭包执行期间。
四、库代码(lore-base / lore / lore-revision / lore-notification)用法
涉及 crate:lore-base、lore、lore-revision、lore-notification。
这些库 crate 直接使用lore_spawn!宏即可,上下文传播是自动的。以 lore/src/call_delegation.rs 的异步入口为例:
let args = args.clone(); drop(lore_base::lore_spawn!(handler(globals, args, callback)));异步 FFI 入口把 handler 以任务形式生成后立即drop返回句柄,由生成的异步任务在运行时上执行并向回调投递Complete/End事件;参数校验失败时同样以lore_spawn!生成拒绝任务(call_delegation.rs)。这里的drop表明生成的任务由运行时托管、不依赖调用者持有句柄,这在库 API 中很典型。
五、并行任务:优先 JoinSet,配合 lore_drain_tasks!
标准明确规定:多任务协调场景优先使用JoinSet,而不是散落保存多个JoinHandle。原因在于JoinSet统一管理任务的加入与逐个取回结果,天然支持「全部完成后再汇总」与「先到先取」两种消费模式。
- 简单并行操作(只关心成败、不关心每个结果)用
lore_drain_tasks!,它提供首个错误语义:排空整个JoinSet,返回遇到的第一个错误,同时让所有任务跑完; - 需要逐个处理结果时,手动用
join_next().await排空。
lore_drain_tasks!的定义在 runtime.rs:
macro_rules! lore_drain_tasks { ($tasks:expr, $join_err:expr) => {{ { let mut __failure = None; while let Some(__res) = $tasks.join_next().await { __failure = __failure.or(__res.map_err(|_| $join_err).flatten().err()); } match __failure { Some(e) => Err(e), None => Ok(()), } } }}; }注意$join_err参数:join_next()返回的Result<T, JoinError>中的JoinError是外来错误,需要先映射成你自己的错误类型,lore_drain_tasks!把这个映射点留给调用方。而lore_limit_drain_tasks!(tasks, max, err)(runtime.rs)则是带并发上限的排空:先用非阻塞的try_join_next()立即收割已完成任务,直到剩余任务数低于max,再await阻塞排空,避免无界堆积。push 分片、并行读取等批量操作场景大量使用这些模式,例如 lore-revision/src/branch/push.rs 中tasks.join_next().await的逐任务收集写法。
六、任务取消:AbortOnDropHandle
对不应超出其作用域的后台任务,标准要求使用tokio_util的AbortOnDropHandle:把lore_spawn!的返回值包进AbortOnDropHandle::new(...),句柄被 drop 时任务自动 abort。
仓库中的真实用例是 push 分片的进度上报 ticker(lore-revision/src/branch/push.rs):
let ticker = AbortOnDropHandle::new(lore_spawn!(async move { let mut ticker = tokio::time::interval(progress_interval); loop { ticker.tick().await; event::LoreEvent::BranchPushFragmentProgress(ticker_progress.event()).send(); } })); if !dry_run { push_fragments(repository, storage, fragments, progress.clone()).await?; } drop(ticker);这个 ticker 是典型的「作用域内后台任务」:它只服务于当前 push 调用,分片传输一结束就必须停止。lore_spawn!返回的JoinHandle被AbortOnDropHandle::new(...)包裹后,drop(ticker)即自动 abort 掉周期性事件上报,随后 push 代码发送最终进度事件收尾。
七、错误处理:把 JoinError 映射进你的错误集
任务可能因 panic 或取消而失败,join_next().await返回的JoinError属于外来错误,标准要求通过.internal("...")把它映射到你的错误类型(这对应 lore-error-set 的#[ffi_code]/#[error_set]体系,参见 错误处理标准 errors.md)。lore_drain_tasks!正是「收集JoinSet首个错误、同时让所有任务跑完」的简写形式;而lore_limit_drain_tasks!则进一步限制了并发排空规模。映射时注意:
JoinError不携带业务错误本身,只说明任务 panic 或被取消,所以.internal("...")的说明文案应当描述「任务侧失败」这一事实;- 若任务 future 的
Output本身是Result,lore_drain_tasks!内部的.flatten()会把Result<Result<T, E>, JoinError>展平,从而保留真正的业务错误作为首个失败。
八、服务端(lore-server)用法:入口点与传播的边界
lore_spawn!在任何已经设置了LORE_CONTEXT的任务内部都可用。服务端绝大多数 handler 代码满足这一条件:gRPC 与 QUIC 入口在分派前就建立了执行上下文(相关入口在 lore-server/src/grpc 与 lore-server/src/quic 目录下)。
标准划出的边界是:
- 仅在入口点(例如 gRPC handler 顶层、QUIC 连接 accept、后台服务初始化)需要手动
LORE_CONTEXT.scope()+runtime().spawn(),因为这些地方要建立新的执行上下文; - 一旦进入 scoped 任务内部,子任务既可以用
lore_spawn!(自动传播),也可以用runtime().spawn(LORE_CONTEXT.scope(execution_context(), ...))(显式传播)。
LORE_CONTEXT.scope(execution, body).await的完整形态可参考 lore-revision/src/commit.rs 测试辅助中的用法,它把ExecutionContext放入作用域后驱动 future 完成。
九、外部服务 crate(lore-aws / lore-hashicorp)用法
lore-aws、lore-hashicorp这类外部服务 crate不参与LORE_CONTEXT传播,可以使用裸tokio::spawn或joinset.spawn()——这也是 clippy 禁令在lore-aws等 crate 中存在的豁免前提(仓库确实在 lore-aws/src/dynamodb.rs 等处使用lore_spawn!配合JoinSet管理查询任务)。
但有一个硬性要求:凡是tracing作为 workspace 依赖的 crate(例如lore-aws),生成的任务必须使用.in_current_span()。否则生成的任务会运行在脱离的 span 中,丢失回源请求的 trace 链路。lore-aws的实现严格遵守这一点,例如 lore-aws/src/dynamodb.rs 与 lore-aws/src/store/mutable_store.rs 中的.in_current_span()调用。
lore-aws还展示了lore_spawn_net!在 HTTP 连接器中的用法(lore-aws/src/net_http_client.rs):
match lore_spawn_net!(async move { inner.call(request).await }).await { Ok(result) => result, // Only a panic or an abort in the dispatch task; nothing aborts it. Err(join_error) => Err(ConnectorError::other(Box::new(join_error), None)), }lore_notification的订阅调用同样走lore_spawn_net!(lore-notification/src/client.rs),说明「网络密集的任务放到网络运行时」是跨 crate 的一致约定。
十、运行时与定向宏:core / net 双运行时下的任务落点
理解lore_spawn_net!与lore_spawn_core!需要先了解 Lore 的运行时划分。根据 tokio 运行时拆分与异步 IO 提案 与 runtime.rs 的实现,Lore 进程内维护两个 tokio 运行时:
- 核心运行时(core runtime):承载计算与文件 IO 的延续(文件 IO 实际经 lore-io syscall 池分派,见 file-io-engine.md);阻塞池固定为 2 线程(runtime.rs),worker 数默认取处理器数;
- 网络运行时(net runtime):承载 quinn/tonic 驱动任务、传输循环与流多路复用,worker 默认 2 线程(客户端),服务端启动时按每处理器一线程配置(runtime.rs);其阻塞池被钉死为 1 线程,防止一个游离的
spawn_blocking饿死协议定时器。
由此:
lore_spawn!跟随当前运行时生成任务;lore_spawn_net!专用于 quinn/tonic 构造、传输循环、流多路复用,永远不要用于计算或文件 IO;lore_spawn_core!用于传输到 handler 的边界:若在 net 上运行的代码用lore_spawn!生成任务,任务会留在 net,其后代也会留在 net,从而把计算和文件 IO 压到 net 的单个阻塞线程上;而 handler 代码内部用普通lore_spawn!即可,因为它从调用方继承 core。lore_spawn_net_nocontext!则用于比命令活得更久的传输任务(如连接生命周期任务):它不传播LORE_CONTEXT,因为上下文属于单条命令,连接级任务若捕获上下文,会在服务后续每条命令时错误地持续上报首条命令的关联 ID——这是「误归属」而非「归属」。
需要警惕的坑:net 运行时的max_blocking_threads(1)意味着从 net 任务发起的阻塞调用会饿死后续所有 net 侧阻塞调用乃至 QUIC / HTTP/2 定时器,因此lore_spawn_blocking!系列总是把阻塞工作落到 core 的阻塞池。这一点有专门测试守护:runtime.rs 验证了从 net 任务发起lore_spawn_blocking!时,工作线程名以lore-tokio-(core)开头而非lore-net-(net)。
十一、guarded 任务与优雅关停
lore_spawn_guarded!(task)生成的任务会被记入进程级的RUNTIME_GUARD(OnceLock<Mutex<JoinSet<()>>>,runtime.rs),并在runtime_flush_guarded()或runtime_shutdown_timeout()期间被 await 到完成。这类任务通常是「必须赶在运行时销毁前收尾的工作」——例如落盘中的写入。runtime_shutdown_timeout(wait_timeout)(runtime.rs)的关停顺序有讲究:先 flush 所有 guarded 任务(有意不设超时,因为它们是必须完成的工作),再对 core 运行时执行shutdown_timeout,最后关停 net 运行时——因为 guarded 的 core 任务可能还在通过网络刷写数据。整个关停由lore_shutdown()(C FFI)等同步路径触发,会经过shutdown_block_on(runtime.rs)在无运行时、多线程运行时、current_thread运行时三种上下文中分别采用block_on、block_in_place或独立线程驱动的方式完成。
十二、六条最佳实践(速查)
- 库代码一律用
lore_spawn!,以获得自动上下文传播; - 服务端代码在上下文已设置时用
lore_spawn!,仅在入口点手动LORE_CONTEXT.scope(); - 多任务协调优先
JoinSet,不要散存多个JoinHandle; - CPU 密集工作用
lore_spawn_blocking!,避免阻塞异步运行时(阻塞变体自动落到 core 阻塞池); - 凡
tracing为依赖的 crate,生成任务一律.in_current_span(),保住 trace 父级关系; - 处理
JoinError:把任务的 panic 与取消通过.internal("...")映射进你的错误集。
十三、与相邻标准的衔接
任务调度标准不是孤立的:它的.internal("...")映射依赖 错误处理标准 的#[error_set]体系;任务内的日志级别与tracing/ Lore 宏的选择见 日志标准;而 测试标准 testing.md 规定所有异步测试使用LORE_CONTEXT.scope()模式建立执行上下文(testing.md),这保证了测试路径与生产路径共享同一套上下文语义。若你想在测试中验证运行时行为,可参考 lore-revision/tests/runtime.rs 与 lore-base/tests/task_lifecycle_observer.rs 中的模式。
一句话总结:在 Lore 代码库中,任务生成不是一个自由动作,而是一套有强制工具链背书(clippy 禁令 + 宏族统一入口 + 生命周期观测)的工程纪律——掌握lore_spawn!宏族与LORE_CONTEXT的传播边界,是写出正确、可观测、可安全关停的 Lore 异步代码的前提。
- 版本控制
- 后端
【免费下载链接】lore
Lore is a next-generation, open source version control system
相关推荐
Archipelago任务调度:异步处理与后台任务
Archipelago任务调度:异步处理与后台任务 概述 Archipelago是一个多游戏随机化框架,其核心功能依赖于高效的异步任务调度和后台处理机制。本文将
游戏开发后端JavaScript异步高级模式实战:宏任务微任务调度、并发控制与性能优化
JavaScript异步高级模式实战:宏任务微任务调度、并发控制与性能优化 本文源于 ColumnWriter 智能体 https://link.gitcode
教程人工智能大模型AI Agent终极Python Mastery异步任务调度指南:从基础到实战的完整教程
终极Python Mastery异步任务调度指南:从基础到实战的完整教程 Python Mastery是一个专注于高级Python编程技巧的开源项目,提供了丰富
示例工程教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考