Rivet RunnersApi 完全指南:使用 Rust SDK 列出与管理 Runner 状态
【免费下载链接】actorsRivet Actors are the primitive for stateful workloads. Built for AI agents, collaborative apps, and durable execution.项目地址: https://gitcode.com/GitHub_Trending/riv/actors
本文围绕 Rivet Actors 仓库中 Rust SDK 自动生成的 RunnersApi 客户端文档展开,系统讲解runners_list(GET /runners)与runners_list_names(GET /runners/names)两个端点的完整使用方式。你将掌握如何通过 Rust SDK 查询命名空间下的 Runner(计算实例)列表、按名称或 ID 过滤、包含已停止实例、基于游标分页,以及理解从 API 网关到 pegboard 底层键值存储的完整调用链路。
什么是 Runner 与 RunnersApi
在 Rivet Actors 体系中,Runner 是承载 Actor 实例运行的计算载体。一个 Runner 拥有固定的资源槽位(slot),每个 Actor 实例会占据其中若干槽位;remaining_slots与total_slots之间的差值即反映了该 Runner 当前的负载状况。RunnersApi 提供了对 Runner 进行只读查询的两个端点,用于运维观测、调度分析和集群管理。
RunnersApi 的全部方法如下(所有 URI 均相对于http://localhost):
| 方法 | HTTP 请求 | 说明 |
|---|---|---|
runners_list | GET/runners | 列出命名空间下的 Runner 列表 |
runners_list_names | GET/runners/names | 列出命名空间下所有 Runner 的名称(去重聚合) |
从仓库结构看,这两个端点存在两层 API:api-public(engine/packages/api-public/src/runners.rs)负责对客户端开放、认证并跨数据中心聚合;api-peer(engine/packages/api-peer/src/runners.rs)则是单数据中心内部的 peer 接口。Rust SDK 客户端直连的是 public 层。
在 Cargo 工程中接入 SDK
RunnersApi 属于rivet-api-full这个 OpenAPI 生成的 Rust 客户端包(API 版本 2.3.14),源码位于 engine/sdks/rust/api-full/rust。接入方式是在Cargo.toml的[dependencies]中加入本地路径依赖:
[dependencies] rivet-api-full = { path = "./rivet-api-full" } tokio = { version = "1", features = ["full"] }初始化客户端需要构造Configuration。该结构体定义在 engine/sdks/rust/api-full/rust/src/apis/configuration.rs,包含以下关键字段:
| 字段 | 类型 | 用途 |
|---|---|---|
base_path | String | API 服务地址,默认http://localhost |
user_agent | Option<String> | 附加到请求的 User-Agent 头 |
client | reqwest::Client | HTTP 客户端 |
bearer_access_token | Option<String> | Bearer 认证令牌 |
示例:
use rivet_api_full::apis::{configuration::Configuration, runners_api}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let configuration = Configuration { base_path: "https://api.example.com".to_string(), user_agent: Some("my-rivet-client/1.0".to_string()), client: reqwest::Client::new(), bearer_access_token: Some("YOUR_TOKEN".to_string()), ..Default::default() }; let response = runners_api::runners_list(&configuration, "my-namespace", None, None, None, None, Some(10), None).await?; println!("{:?}", response.runners); Ok(()) }认证方式
两个端点都要求bearer_auth认证(见文档中的 Authorization 一节)。SDK 在发起请求时会自动读取configuration.bearer_access_token并附加Authorization: Bearer <token>头,见 engine/sdks/rust/api-full/rust/src/apis/runners_api.rs:
if let Some(ref token) = configuration.bearer_access_token { req_builder = req_builder.bearer_auth(token.to_owned()); };服务端在 engine/packages/api-public/src/runners.rs 中通过ctx.auth().await?校验令牌。
请求头约定
- Content-Type:未定义(GET 请求无请求体)
- Accept:
application/json
SDK 按Accept: application/json发送请求,收到响应后根据 content-type 反序列化:application/json解析为对应模型;text/plain或不支持的类型会返回显式错误(见 runners_api.rs 中的错误处理逻辑)。
runners_list:列出 Runner
函数签名(SDK 侧):
pub async fn runners_list( configuration: &configuration::Configuration, namespace: &str, name: Option<&str>, runner_ids: Option<&str>, runner_id: Option<Vec<String>>, include_stopped: Option<bool>, limit: Option<i32>, cursor: Option<&str>, ) -> Result<models::RunnersListResponse, Error<RunnersListError>>参数说明
服务端查询参数定义于 engine/packages/api-types/src/runners/list.rs 的ListQuery结构:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
namespace | String | 是 | 命名空间名称(注意是全局可读的名称,而非 ID) |
name | Option<String> | 否 | 按 Runner 名称精确过滤 |
runner_ids | Option<String> | 否 | 已废弃。逗号分隔的 Runner ID 列表 |
runner_id | Option<Vec<String>> | 否 | 按 Runner ID 过滤,支持多个;SDK 以multi风格重复runner_id查询参数发送 |
include_stopped | Option<bool> | 否 | 是否包含已停止的 Runner,默认false |
limit | Option<i32> | 否 | 返回条数上限,服务端默认100 |
cursor | Option<String> | 否 | 分页游标,取自上一页响应的pagination.cursor |
注意 SDK 实现细节:runner_id参数使用 "multi" 收集方式,每个 ID 单独作为一个runner_id查询参数发送(runners_api.rs):
req_builder = match "multi" { "multi" => req_builder.query(¶m_value.into_iter() .map(|p| ("runner_id".to_owned(), p.to_string())) .collect::<Vec<_>>()), _ => /* join with comma */, };两条查询路径
从服务端实现(engine/packages/api-peer/src/runners.rs)可以清楚看到,runners_list内部会根据是否提供了 ID 走两条完全不同的路径:
路径 A:按 ID 精确查询
当传入runner_id(或已废弃的runner_ids)时,服务端直接把 ID 交给pegboard::ops::runner::get逐条拉取 Runner,返回的pagination.cursor为None(无分页语义)。
路径 B:按条件列出
否则调用pegboard::ops::runner::list_for_ns,携带namespace_id、name、include_stopped、created_before(由cursor解析而来)与limit,并基于最后一个 Runner 的create_ts生成下一页游标:
let cursor = list_res.runners.last().map(|x| x.create_ts.to_string());使用示例
let response = runners_api::runners_list( &configuration, "my-namespace", // namespace 必填 Some("worker-us-1"), // 按名称过滤(可选) None, // runner_ids 已废弃,传 None Some(vec!["r_abc123".into()]), // 按 ID 过滤(可选) Some(true), // 包含已停止的 Runner Some(50), // limit None, // cursor ).await?; for runner in &response.runners { println!("{}: slots {}/{} dc={}", runner.runner_id, runner.remaining_slots, runner.total_slots, runner.datacenter); } // 下一页 if let Some(cursor) = response.pagination.cursor { let next = runners_api::runners_list( &configuration, "my-namespace", None, None, None, Some(true), Some(50), Some(&cursor) ).await?; }返回值:RunnersListResponse
响应模型见 engine/sdks/rust/api-full/rust/docs/RunnersListResponse.md:
| 字段 | 类型 | 说明 |
|---|---|---|
pagination | models::Pagination | 游标分页信息 |
runners | Vec<models::Runner> | Runner 列表 |
Pagination结构极为精简,仅包含一个可选的cursor: Option<String>(定义见 engine/packages/api-types/src/pagination.rs)。
Runner模型的字段(见 engine/sdks/rust/api-full/rust/docs/Runner.md 与 engine/packages/types/src/runners.rs):
| 字段 | 类型 | 说明 |
|---|---|---|
runner_id | String | Runner 唯一 ID |
namespace_id | String | 所属命名空间 ID |
datacenter | String | 所在数据中心 |
name | String | Runner 名称 |
key | String | Runner 密钥 |
version | i32 | 版本号 |
total_slots/remaining_slots | i32 | 总槽位 / 剩余槽位 |
create_ts | i64 | 创建时间戳(毫秒) |
drain_ts | Option<i64> | 排空(drain)时间戳,可选 |
stop_ts | Option<i64> | 停止时间戳,可选 |
last_ping_ts | i64 | 最近一次心跳时间戳 |
last_connected_ts | Option<i64> | 最近连接时间戳,可选 |
last_rtt | i32 | 最近一次往返时延 |
metadata | Option<serde_json::Value> | 附加元数据,可选 |
runners_list_names:列出 Runner 名称
当只需要名称清单(例如构建一个名称选择器或做存在性检查)时,使用该端点可以显著降低数据传输量。
函数签名(SDK 侧):
pub async fn runners_list_names( configuration: &configuration::Configuration, namespace: &str, limit: Option<i32>, cursor: Option<&str>, ) -> Result<models::RunnersListNamesResponse, Error<RunnersListNamesError>>参数说明
服务端参数定义于 engine/packages/api-types/src/runners/list_names.rs 的ListNamesQuery:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
namespace | String | 是 | 命名空间名称 |
limit | Option<i32> | 否 | 返回名称数量上限,服务端默认100 |
cursor | Option<String> | 否 | 分页游标 |
注意与runners_list不同,该端点没有name、runner_id等过滤参数,只支持分页游标遍历。
Datacenter Round Trips:2 次往返
文档在该端点下方标注了关键的实现提示,源码同样体现在 engine/packages/api-public/src/runners.rs:
2 round trips: - GET /runners/names (fanout) - [api-peer] namespace::ops::resolve_for_name_global这意味着一次runners_list_names调用实际包含两次数据中心往返:
- public 层通过
fanout_to_datacenters将查询**扇出(fanout)**到所有数据中心各自的 peer 接口GET /runners/names; - 每个数据中心内,peer 处理器先调用
namespace::ops::resolve_for_name_global将命名空间名称解析为namespace_id(见 engine/packages/api-peer/src/runners.rs),再执行名称扫描。
使用示例
let response = runners_api::runners_list_names( &configuration, "my-namespace", Some(100), // limit None, // cursor ).await?; for name in &response.names { println!("runner name: {name}"); }返回值:RunnersListNamesResponse
响应模型见 engine/sdks/rust/api-full/rust/docs/RunnersListNamesResponse.md:
| 字段 | 类型 | 说明 |
|---|---|---|
names | Vec<String> | Runner 名称列表 |
pagination | Pagination | 分页游标 |
public 层在聚合时会先对各数据中心返回的名称做去重(IndexSet),随后排序并截断到limit,最后以最后一个名称作为下一页游标(见 api-public/src/runners.rs):
let mut all_names = fanout_to_datacenters::<_, _, _, _, _, IndexSet<String>>( &ctx, "/runners/names", query, |ctx, query| async move { rivet_api_peer::runners::list_names(ctx, (), query).await }, |_, res, agg| agg.extend(res.names), ).await?.into_iter().take(limit).collect::<IndexSet<_>>(); all_names.sort(); let cursor = all_names.last().map(|x: &String| x.to_string());服务端与底层的完整调用链
把两个端点串联起来,一次查询在仓库中的完整调用链为:
Rust SDK (runners_api.rs) │ GET /runners 或 /runners/names(Bearer 认证) ▼ api-public 处理器 (engine/packages/api-public/src/runners.rs) │ fanout_to_datacenters(扇出到所有数据中心) ▼ api-peer 处理器 (engine/packages/api-peer/src/runners.rs) │ namespace::ops::resolve_for_name_global → namespace_id ▼ pegboard operation (engine/packages/pegboard/src/ops/runner/) ├── list_for_ns.rs —— 列表扫描 + get_inner 回填 └── list_names.rs —— 名称键范围扫描 ▼ universaldb 事务(Snapshot 隔离级别) └── 键空间:keys::ns::ActiveRunnerKey / AllRunnerKey / RunnerNameKey ...pegboard 列表扫描实现
pegboard_runner_list_for_ns(engine/packages/pegboard/src/ops/runner/list_for_ns.rs)按四种组合分支扫描键空间,每种分支都对应独立的索引键类型:
| 条件 | 使用的键类型 |
|---|---|
| 按名称 + 包含停止 | AllRunnerByNameKey |
| 按名称 + 仅活跃 | ActiveRunnerByNameKey |
| 无名称 + 包含停止 | AllRunnerKey |
| 无名称 + 仅活跃 | ActiveRunnerKey |
所有扫描都以Snapshot(快照)隔离级别执行——代码注释明确说明“列表时旧数据无关紧要,无需 Serializable”。扫描方向为reverse: true,配合created_before(由游标解析)实现基于创建时间的倒序分页;取到 runner_id 后通过super::get::get_inner以buffered(512)并发回填完整的 Runner 数据。
pegboard 名称扫描实现
pegboard_runner_list_names(engine/packages/pegboard/src/ops/runner/list_names.rs)基于RunnerNameKey做正向范围扫描:若提供了after_name游标,则以end_of_key_range从该名称之后开始,StreamingMode::Exact+limit精确控制返回条数。同样使用 Snapshot 隔离级别,注释说明这是为了避免与新名称插入产生争用("not Serializable to prevent contention with inserting new names")。
键空间结构
这些索引键定义于 engine/packages/pegboard/src/keys/ns.rs:
ActiveRunnerKey:(NAMESPACE, namespace_id, RUNNER, ACTIVE, create_ts, runner_id)—— 活跃 Runner 主索引;AllRunnerKey:(NAMESPACE, namespace_id, RUNNER, ALL, create_ts, runner_id)—— 全部 Runner(含已停止);RunnerNameKey:(NAMESPACE, namespace_id, RUNNER, NAME, name)—— 名称去重索引(值为空,仅作存在性标记)。
键的前缀 tuple 结构意味着同一命名空间、同一前缀下的记录在存储中天然按create_ts排序,这正是游标分页可以直接把create_ts字符串当作下一页起点、并用end_of_key_range截断的底层原因。
游标分页与 limit 语义
两个端点都遵循相同的分页约定:
- 请求时传入上一页返回的
pagination.cursor; - 服务端把游标解析为上一页最后一个元素的排序键(
runners_list用create_ts,runners_list_names用名称); - 底层扫描从该键之后开始,因此游标分页不会因新数据插入而重复或遗漏(区别于 offset 分页);
- 首页请求不传
cursor,从最新记录开始。
一个容易踩的坑:runners_list走“按 ID 查询”路径时返回的cursor恒为None,此时继续分页没有意义;只有走“列表扫描”路径才会产生有效的下一页游标。
常见错误处理
SDK 为每个方法定义了类型化错误枚举(见 runners_api.rs):
RunnersListError/RunnersListNamesError:均只有一个UnknownValue(serde_json::Value)变体,用于承接服务端返回的非预期错误体。
当 HTTP 状态码为 4xx/5xx 时,SDK 返回Error::ResponseError(ResponseContent { status, content, entity }),其中content是原始响应文本,entity是尝试反序列化出的错误枚举。实战中应优先检查status判断错误类别。
典型失败场景:
| 场景 | 表现 |
|---|---|
未提供bearer_access_token | 服务端ctx.auth()校验失败,返回 401 |
| 命名空间不存在 | resolve_for_name_global返回None,映射为Namespace::NotFound(见 api-peer/src/runners.rs) |
runner_ids中含有非法 ID | 解析失败,返回ApiBadRequest("invalid id inrunner_idsquery") |
响应不是application/json | SDK 返回反序列化错误 |
与 Actors 查询 API 的关系
RunnersApi 与仓库中同样由 OpenAPI 生成的 ActorsListApi(GET /actors)是配套关系:Actor 运行在 Runner 之上,因此常见的数据流是先用runners_list找到负载较低的 Runner,再结合actors_list观察其上的 Actor 分布;而runners_list_names适合做名称空间的全局普查。两者共享同一套Pagination游标约定与 fanout 架构,理解本文的调用链后即可举一反三。
小结
本文基于 engine/sdks/rust/api-full/rust/docs/RunnersApi.md 及其对应的服务端实现,完整梳理了 RunnersApi 两个端点的参数、返回值、分页机制与底层实现:
runners_list(GET /runners)返回RunnersListResponse,支持按名称、按 ID 过滤,以及include_stopped、limit、cursor参数;服务端存在“按 ID 直查”和“按索引扫描”两条路径;runners_list_names(GET /runners/names)返回RunnersListNamesResponse,只含名称清单,跨数据中心扇出并去重排序,全程 2 次数据中心往返;- 两个端点均需 bearer_auth,分页统一采用基于
create_ts/ 名称的游标机制; - 底层由 pegboard 的 universaldb 键扫描支撑,索引键设计(ACTIVE/ALL/NAME 前缀)直接决定了查询的排序与分页能力。
结合 engine/packages/api-public/src/runners.rs 与 engine/packages/api-peer/src/runners.rs 阅读,即可获得从 SDK 到存储的完整视角。
【免费下载链接】actorsRivet Actors are the primitive for stateful workloads. Built for AI agents, collaborative apps, and durable execution.项目地址: https://gitcode.com/GitHub_Trending/riv/actors
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考