rusty-cat 分片并发上传完全指南

5 阅读22分钟

rusty-cat 分片并发上传完全指南(Rust 小白版)

本文面向 刚接触 Rust、想快速把「大文件分片并发上传」跑起来 的开发者。 每段代码都可以直接复制到你的项目里编译运行,遇到需要替换的地方都写了中文注释。 配套阅读:《rusty-cat 分片并发下载完全指南》


目录

  1. rusty-cat 是什么,能帮你解决什么问题
  2. 第一步:在 Cargo.toml 里加依赖
  3. 10 分钟跑通第一个上传(完整可运行程序)
  4. 核心概念:两个「并发」参数千万别搞混
  5. 开启「单文件分片并发」——with_max_parts_in_flight
  6. 四种云存储上传配方(各有独立实战文章)
  7. 从内存字节上传 / 自建服务器上传
  8. 进度、暂停、恢复、取消
  9. 断点续传:进程重启后接着传
  10. 内存占用与参数怎么选
  11. 错误处理与自动重试
  12. 常见问题排查表(FAQ)
  13. 附录:配置项速查表

1. rusty-cat 是什么,能帮你解决什么问题

rusty-cat 是一个 异步(async)的 Rust 文件传输 SDK,专门解决「大文件的可断点续传上传 / 下载」。它帮你处理这些又琐碎又容易出 bug 的事:

  • 分片(chunk):把一个大文件切成很多小块分别上传;
  • 并发(concurrent):同一个文件的多个分片可以同时上传,把带宽跑满;
  • 重试 / 退避:网络抖动时自动重试,且用指数退避 + 抖动,不会「重试风暴」;
  • 暂停 / 恢复 / 取消:随时控制任务;
  • 进度回调:实时把进度百分比推给你的 UI;
  • 断点续传:进程被杀掉、崩溃、重启后能接着传,而不是从头再来。

不帮你做 的事(这是刻意的设计,让 SDK 保持小巧、适配任意后端):

  • 不内置数据库(要不要持久化任务记录、用什么库,你自己决定);
  • 不管理你的云密钥 / 权限(你从自己的后端提供)。

支持的上传后端:普通 HTTP、阿里云 OSS(直传 / 预签名)、Azure Blob(直传 / SAS)。

一句话记住它的用法:创建 MeowClient → 用 Builder 构建一个「上传任务」→ 提交给客户端 → 收进度回调 → 用完 close()


2. 第一步:在 Cargo.toml 里加依赖

rusty-cat 用「feature(特性开关)」来控制引入哪些云厂商的代码,你只需要开启你用到的那一个,包体积和依赖都最小。

[dependencies]
# 按需选一个(或多个)云厂商特性:
rusty-cat = { version = "0.2.4", features = ["aliyun-oss-direct"] }
# rusty-cat = { version = "0.2.4", features = ["aliyun-oss-presigned"] }
# rusty-cat = { version = "0.2.4", features = ["azure-blob-direct"] }
# rusty-cat = { version = "0.2.4", features = ["azure-blob-sas"] }

# rusty-cat 是 async 库,需要一个异步运行时,这里用 tokio:
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

各个 feature 的含义:

Feature用途谁持有密钥
aliyun-oss-direct阿里云 OSS 直传,客户端直接用 AccessKey 签名客户端(受信任的环境,如你自己的服务器)
aliyun-oss-presigned阿里云 OSS 预签名,后端签好短期 URL,客户端只负责传后端
azure-blob-directAzure Blob 直传,用账号 Shared Key 签名客户端(受信任环境)
azure-blob-sasAzure Blob SAS,后端签好短期 SAS URL后端

怎么选? 如果你的程序跑在不完全受信任的环境(比如用户的手机、桌面 App、浏览器端 WASM),优先用预签名 / SAS:密钥永远留在后端,客户端只拿到几分钟有效的临时 URL。只有当程序跑在你完全掌控的服务器上时,才考虑直传(direct)。


3. 10 分钟跑通第一个上传(完整可运行程序)

下面是一个完整main.rs,演示把本地文件分片并发上传到阿里云 OSS。把顶部四个常量换成你自己的,cargo run 即可。

需要在 Cargo.toml 里开启 features = ["aliyun-oss-direct"]

use std::sync::Arc;
use std::time::Duration;

use rusty_cat::aliyun_oss_direct::AliOssDirectUpload;
use rusty_cat::api::{FileTransferRecord, MeowClient, MeowConfig, UploadPounceBuilder};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    // ===== 改成你自己的信息 =====
    let bucket = "my-bucket";                 // OSS Bucket 名
    let access_key_id = "AK_ID";              // AccessKeyId
    let access_key_secret = "AK_SECRET";      // AccessKeySecret
    let region = "cn-beijing";                // 地域,例如 cn-beijing / cn-hangzhou
    let local_file = "./big-object.bin";      // 要上传的本地文件
    let object_url =                          // 上传到 OSS 后的对象完整 URL
        "https://my-bucket.oss-cn-beijing.aliyuncs.com/test/big-object.bin";
    // ============================

    // 1) 创建配置和客户端
    let config = MeowConfig::builder()
        .max_upload_concurrency(3)            // 同时最多传 3 个「文件」
        .http_timeout(Duration::from_secs(30))
        .build()?;
    let client = MeowClient::new(config);

    // 2) 准备上传协议(阿里云 OSS 直传)
    let protocol =
        AliOssDirectUpload::new(bucket, access_key_id, access_key_secret, region);

    // 3) 构建上传任务
    let task = UploadPounceBuilder::new(
        "big-object.bin",   // 展示用的文件名(回调 / 日志里能看到)
        local_file,         // 本地文件路径
        1024 * 1024,        // 分片大小 = 1 MiB
    )
    .with_url(object_url)
    .with_breakpoint_upload(Arc::new(protocol)) // 协议对象必须放进 Arc
    .with_max_parts_in_flight(4)                // 同一个文件最多 4 片并发上传
    .with_max_chunk_retries(3)                  // 每片失败最多再重试 3 次
    .build()?;

    // 4) 提交任务并等待它跑完(enqueue_and_wait 是最省事的方式)
    let outcome = client
        .enqueue_and_wait(task, |record: FileTransferRecord| {
            println!(
                "上传进度 {:.1}%  状态 {:?}",
                record.progress() * 100.0,
                record.status()
            );
        })
        .await?;

    println!(
        "上传完成!task_id={} 服务端返回={:?}",
        outcome.task_id, outcome.payload
    );

    // 5) 关闭客户端(这一步是必须的!见下方说明)
    client.close().await?;
    Ok(())
}

几个新手最容易忽略的点:

  • enqueue_and_wait(task, progress_cb) 是最简单的提交方式:它提交任务、在每次进度更新时调用你的回调、并在任务到达「终态」时返回。成功返回 TaskOutcome,失败 / 被取消会返回 Err(MeowError),所以你直接用 ? 就能处理错误。
  • 协议对象要放进 ArcArc::new(protocol)):因为并发上传时,同一个协议对象 &self 会被多个异步任务共享。
  • with_max_parts_in_flight(4) 就是「单文件分片并发」开关,第 5 节详细讲。
  • 一定要调用 client.close().await:它会取消在途任务、把缓冲的回调发完、并优雅地关闭后台调度线程。不要依赖 Drop 自动清理——那只是兜底,会丢事件。

进度是个 0.0~1.0 的比例record.progress() 返回 f32,乘 100 就是百分比。没有「已传字节数」的直接读取方法,如需字节数请自己算:(f64::from(record.progress()) * record.total_size() as f64) as u64


4. 核心概念:两个「并发」参数千万别搞混

这是新手 第一大坑。rusty-cat 有 两个完全独立 的并发旋钮:

旋钮作用范围默认值控制什么
MeowConfig::builder().max_upload_concurrency(k)整个客户端2同时上传 几个文件
UploadPounceBuilder::with_max_parts_in_flight(n)单个文件内部1一个文件的 几个分片 同时传
  • max_upload_concurrency(k):客户端级别,控制多少个文件同时在传。上传和下载各自独立计数、互不占用。
  • with_max_parts_in_flight(n):任务级别,控制同一个文件的多少个分片同时在传(这才是本文的主角)。

两者会相乘。如果 max_upload_concurrency(3),且每个文件都 with_max_parts_in_flight(4),那么客户端最多会有 3 × 4 = 12 个上传请求同时在飞。没有全局上限帮你兜底,请自己把乘积控制在合理范围。

🖼️ 【配图占位 U2|两个并发旋钮的层级关系】 建议配图:外层一个大框「客户端」,标 max_upload_concurrency(3),里面并排 3 个「文件」框;每个文件框内再画 4 个并行的「分片」块,标 with_max_parts_in_flight(4)。图注:外层控制「几个文件」、内层控制「一个文件几个分片」,两者相乘 = 最多 12 个请求在飞。 临时 ASCII 草图:

             ┌─ 文件A ─ 分片1 分片2 分片3 分片4with_max_parts_in_flight(4)
客户端 ──────┼─ 文件B ─ 分片1 分片2 分片3 分片4
             └─ 文件C ─ 分片1 分片2 分片3 分片4max_upload_concurrency(3):最多 3 个文件同时传

5. 开启「单文件分片并发」——with_max_parts_in_flight

默认值是 1,也就是严格串行:一片传完再传下一片,和最经典的单流上传逐字节一致。传入大于 1 的值即开启并发。

什么时候该用?

  • 适合:单个大文件(几十 MiB 以上)、走一条快网(单流跑不满带宽,比如服务器往对象存储推大对象)。
  • 没必要:小文件、单流已经跑满带宽、或者你想省内存的场景,保持默认 1 即可。

加速效果和你的文件大小、网络状况强相关,SDK 没有承诺任何固定的倍数。把它当作一个「可以试、要实测」的工具。

三个前置条件(并发闸门)

设了 with_max_parts_in_flight(8) 并不保证 真的并发。执行器只有在 同时满足以下三条 时才走并发路径:

  1. n > 10 会被归一化成 1);
  2. 文件总大小已知 —— 上传时总大小由本地文件 / 内存字节自动得到,所以上传天然满足这条;
  3. 上传协议是「并发安全」的(下表)。

如果条件 1 或 3 不满足,任务会静默回退到串行:不报错、不告警,照样成功,只是一片一片传。

🖼️ 【配图占位 U3|并发闸门决策流程图】 建议画一张流程图(判断框依次串联,任一为「否」就走串行):n > 1? →否→ 串行;→是→ 总大小已知? →否→ 串行(上传天然满足);→是→ 协议并发安全? →否→ 串行;→是→ 并发。 临时 ASCII 草图:

n>1? ──否──▶ 串行
 │是
总大小已知? ──否──▶ 串行
 │是
协议并发安全? ──否──▶ 串行
 │是
 ▼
并发 ✅

哪些上传协议支持并发?

并发要求协议是「乱序安全」的:分片 N 不依赖分片 N-1 完成,且最后的「合并」步骤能按正确顺序拼装。

上传协议Feature并发安全?说明
AliOssDirectUploadaliyun-oss-direct✅ 是OSS Multipart,分片号由偏移量推导,重传幂等,Complete 按序合并
PresignedMultipartUpload(阿里云预签名)aliyun-oss-presigned✅ 是预签名分片,按偏移记账,完成清单按分片号重排
AzureBlobDirectUploadazure-blob-direct✅ 是Block Blob,block id 由分片下标决定,Put Block List 固定提交顺序
PresignedMultipartUpload(Azure SAS)azure-blob-sas✅ 是同样的 Block Blob 模型,走 SAS URL
内置默认 HTTP 上传(DefaultStyleUpload❌ 否依赖服务端单一 nextByte 游标,必须严格顺序
你自己实现的 BreakpointUpload自定义❌ 否(默认)除非你显式声明支持,否则默认串行

划重点:想要并发上传,就 给任务挂上上面四种 provider 上传协议之一,并设 n > 1。内置的默认 HTTP 上传永远串行。


6. 四种云存储上传配方(各有独立实战文章)

每种云存储的上传配方(含完整可运行程序、内部数据流讲解、断点续传要点)都拆成了独立的实战文章,方便你按需只看用到的那一个:

实战文章Feature谁持密钥一句话
阿里云 OSS 直传直下aliyun-oss-direct客户端可信服务器用 AccessKey 直连 OSS
阿里云 OSS 预签名aliyun-oss-presigned后端后端签短期 URL,客户端只传字节
Azure Blob 直传直下azure-blob-direct客户端可信服务器用 Shared Key 直连 Blob
Azure Blob SASazure-blob-sas后端后端签短期 SAS URL

四种配方共用同一套「构建协议 → with_breakpoint_upload(Arc::new(...)) → 设 with_max_parts_in_flight(4)enqueue_and_wait」骨架,只有「怎么构建协议」不同。最小上手片段(以 OSS 直传为例):

let protocol = AliOssDirectUpload::new("my-bucket", "AK_ID", "AK_SECRET", "cn-beijing");
let task = UploadPounceBuilder::new("big-object.bin", "./big-object.bin", 1024 * 1024)
    .with_url("https://my-bucket.oss-cn-beijing.aliyuncs.com/test/big-object.bin")
    .with_breakpoint_upload(Arc::new(protocol))
    .with_max_parts_in_flight(4) // ← 单文件并发开关
    .build()?;
client.enqueue_and_wait(task, |r| println!("{:.1}%", r.progress() * 100.0)).await?;

🖼️ 【配图占位 U1|四种上传配方的密钥模型对比】 建议配图:左右两栏对比。左「direct(直连)」:客户端图标里有钥匙,直接连云;标注「仅限可信服务器」。右「presigned/SAS」:钥匙在后端图标里,客户端只从后端拿短期 URL、再连云;标注「适合手机/浏览器等不可信端」。 临时 ASCII 草图:

direct          [可信服务器 🔑] ─────────▶ [云存储]
presigned/SAS   [客户端] ─要URL→ [后端 🔑] ─签URL→ [客户端] ─传字节→ [云存储]

并发上传时 SDK 的保证

在并发路径上,没有任何单个分片会去「结束」整个上传。每个分片只管把自己的字节传上去;Complete / Put Block List 这一步被提升到调度器层,只执行一次,且只在所有分片都落地、拼成连续前缀之后才执行。这就是乱序分片安全的原因——先传完的分片不会触发提前完成。


7. 从内存字节上传 / 自建服务器上传

7.1 上传内存里的字节(而不是本地文件)

如果你的数据在内存里(比如刚生成的一段 buffer),用 from_bytes 代替 new

use std::sync::Arc;

use rusty_cat::aliyun_oss_direct::AliOssDirectUpload;
use rusty_cat::api::{MeowClient, MeowConfig, UploadPounceBuilder};

async fn upload_from_memory(
    data: Vec<u8>, // 你内存里的数据
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let client = MeowClient::new(MeowConfig::default());
    let protocol = AliOssDirectUpload::new("my-bucket", "AK_ID", "AK_SECRET", "cn-beijing");

    // from_bytes(展示文件名, 字节, 分片大小)
    let task = UploadPounceBuilder::from_bytes("in-memory.bin", data, 1024 * 1024)
        .with_url("https://my-bucket.oss-cn-beijing.aliyuncs.com/test/in-memory.bin")
        .with_breakpoint_upload(Arc::new(protocol))
        .with_max_parts_in_flight(4)
        .build()?;

    client.enqueue_and_wait(task, |_r| {}).await?;
    client.close().await?;
    Ok(())
}

Vec<u8> 会被零拷贝地转成 bytes::Bytes,后续切片和克隆都只是引用计数 +1,不会复制大块内存。

7.2 上传到你自己的 HTTP 服务器

如果不用云厂商、而是传到你自己的服务器,有两种方式:

  1. 用内置默认协议:不挂任何 provider 协议,直接 build()。它走一套约定好的 multipart/form-data 分片请求格式(默认协议串行,不支持并发)。
  2. 自己实现 BreakpointUpload:完全控制每片的请求方法、URL、Header、Body。下面是一个最小示例,把每片用 PUT 发到某个 URL,并带上 x-offset 头告诉服务器写入位置:
use async_trait::async_trait;
use reqwest::header::{HeaderMap, HeaderValue};
use rusty_cat::api::{
    BreakpointUpload, InnerErrorCode, MeowError, UploadChunkCtx, UploadPrepareCtx,
    UploadResumeInfo,
};

#[derive(Clone)]
struct MyBinaryUpload {
    url: String,
}

#[async_trait]
impl BreakpointUpload for MyBinaryUpload {
    // prepare 在上传开始前调用一次,返回「从第几个字节续传」
    async fn prepare(&self, _ctx: UploadPrepareCtx<'_>) -> Result<UploadResumeInfo, MeowError> {
        Ok(UploadResumeInfo {
            completed_file_id: None,
            next_byte: Some(0), // 0 表示从头开始
            provider_upload_id: None,
        })
    }

    // 每个分片调用一次
    async fn upload_chunk(&self, ctx: UploadChunkCtx<'_>) -> Result<UploadResumeInfo, MeowError> {
        let mut headers = HeaderMap::new();
        headers.insert(
            "x-offset",
            HeaderValue::from_str(&ctx.offset.to_string()).unwrap(),
        );
        let resp = ctx
            .client // SDK 提供的、已按配置构建好的 reqwest::Client
            .put(&self.url)
            .headers(headers)
            .body(reqwest::Body::from(ctx.chunk.clone()))
            .send()
            .await
            .map_err(|e| {
                MeowError::from_source(InnerErrorCode::HttpError, e.to_string(), e)
            })?;
        if !resp.status().is_success() {
            return Err(MeowError::from_code(
                InnerErrorCode::ResponseStatusError,
                format!("上传返回状态 {}", resp.status()),
            ));
        }
        Ok(UploadResumeInfo {
            completed_file_id: None,
            next_byte: Some(ctx.offset + ctx.chunk.len() as u64),
            provider_upload_id: None,
        })
    }
}

然后像 provider 协议一样挂上去(注意自定义协议默认串行):

let task = UploadPounceBuilder::new("http-upload.bin", "./http-upload.bin", 1024 * 1024)
    .with_url("http://127.0.0.1:8080/upload")
    .with_method(reqwest::Method::PUT)
    .with_breakpoint_upload(Arc::new(MyBinaryUpload { url: "http://127.0.0.1:8080/upload".into() }))
    .build()?;

8. 进度、暂停、恢复、取消

8.1 用 try_enqueue 拿到更细的回调(进度 + 完成)

enqueue_and_wait 适合「提交后一直等到结束」。如果你要构建 UI、需要在后台异步跑,用底层的 try_enqueue,它返回 TaskId,并让你分别处理进度回调和完成回调:

use std::sync::Arc;

use rusty_cat::aliyun_oss_direct::AliOssDirectUpload;
use rusty_cat::api::{
    FileTransferRecord, MeowClient, MeowConfig, TaskId, TransferStatus, UploadPounceBuilder,
};

async fn upload_with_callbacks() -> Result<TaskId, Box<dyn std::error::Error + Send + Sync>> {
    let client = MeowClient::new(MeowConfig::default());
    let protocol = AliOssDirectUpload::new("my-bucket", "AK_ID", "AK_SECRET", "cn-beijing");
    let task = UploadPounceBuilder::new("f.bin", "./f.bin", 1024 * 1024)
        .with_url("https://my-bucket.oss-cn-beijing.aliyuncs.com/test/f.bin")
        .with_breakpoint_upload(Arc::new(protocol))
        .with_max_parts_in_flight(4)
        .build()?;

    let task_id = client
        .try_enqueue(
            task,
            // 进度回调:每次进度更新都会调用
            move |record: FileTransferRecord| match record.status() {
                TransferStatus::Failed(err) => eprintln!("上传失败:{err}"),
                TransferStatus::Canceled => println!("已取消"),
                _ => println!("进度 {:.1}%", record.progress() * 100.0),
            },
            // 完成回调:任务成功到达 Complete 时调用一次
            move |task_id, payload| {
                println!("任务 {task_id} 完成,服务端返回 {payload:?}");
            },
        )
        .await?;

    // 保存好 task_id,后面用它来 暂停/恢复/取消
    Ok(task_id)
}

record.status() 返回的是 &TransferStatus,可以直接 matchTransferStatus 的取值有:Pending(排队)、Transmission(传输中)、Paused(暂停)、Complete(完成)、Failed(err)(失败)、Canceled(取消)。

8.2 暂停 / 恢复 / 取消

拿到 TaskId 后就能控制任务:

client.pause(task_id).await?;   // 暂停
client.resume(task_id).await?;  // 从断点继续(TaskId 不变)
client.cancel(task_id).await?;  // 取消(会跑 provider 的清理,比如 OSS 的 AbortMultipartUpload)
  • 取消上传会触发协议的 abort_upload(例如 OSS 的 AbortMultipartUpload),让未提交的分片停止占用存储、不再计费。
  • 取消是尽力而为的、且不可逆——取消后想再传,就重新建一个任务。

8.3 并发下的进度行为(重要且反直觉)

即使分片是乱序完成的,SDK 只上报「最长连续前缀」的进度(业界叫 watermark,水位线):

  • 进度是单调递增的,绝不回退;
  • 如果靠后的分片先传完、但前面还有「空洞」,进度会原地不动,直到空洞被填上——这是正常现象,不是卡死
  • 每次水位线推进,你只会收到一次合并后的进度事件(不是每片一个),终态只有一次。

🖼️ 【配图占位 U4|水位线(watermark)进度模型】 建议配图:一条按分片切格的进度条。分片 1、2、4 已完成(涂实),分片 3 还是空洞。用一条竖线标出「水位线」停在分片 2 结尾处——尽管分片 4 已完成,进度仍只算到连续前缀的末尾。图注:进度 = 连续前缀 / 总大小,单调递增;空洞填上后水位线才前跳。 临时 ASCII 草图:

分片:  [#1✅][#2✅][#3 ⬜空洞][#4✅]
                  ▲
                水位线(进度只算到这里,即使 #4 已完成)

9. 断点续传:进程重启后接着传

rusty-cat 刻意不内置数据库,续传要靠你在自己的库里持久化足够的信息,重启后重建同一个逻辑任务再提交。要点:

  1. 建一张自己的传输表,字段至少包含:业务文件 ID、本地路径、远端 URL / 对象 key、方向(上传/下载)、分片大小、provider 类型、状态、进度等;
  2. 在进度回调里把 FileTransferRecord 落库(不要在回调里做慢速 DB 写,最好丢给你自己的 worker 队列批量写);
  3. 进程重启后,查出未完成的行,重建等价的 PounceTask
  4. 再次调用 try_enqueue(...),provider 协议会从本地 / 远端的断点信息继续。

各 provider 的续传细节:

  • 阿里云 OSS 预签名:用 PresignedMultipartUpload::with_resumed_parts(...) 把上次持久化的分片重新注入,prepare 会跳过已验证的最长连续前缀、重传其余部分。
  • 阿里云 OSS 直传:续传时 SDK 通过 ListMultipartUploads 接管已有的 multipart 会话。建议持久化 provider 的 UploadIdAliOssDirectUpload::current_upload_id()),并在 Bucket 上配一条 AbortMultipartUpload 生命周期规则,让废弃会话自动清理、不再计费。
  • Azure 直传 / SAS:没有 upload id,续传靠恢复服务端的「未提交 block 列表」。未提交的 block 会计费到 Azure 垃圾回收(约 7 天),所以也要配生命周期规则。

还有一个「先导入为暂停态、再让用户选择性开始」的高级用法:用 try_enqueue_paused(...) 把任务导入成 Paused 状态,不发任何网络 / 文件 IO,之后对用户选中的任务调 resume(task_id) 即可。适合「重启后恢复一堆任务,但只启动用户勾选的那几个」。


10. 内存占用与参数怎么选

每个在途分片都需要一个缓冲区,所以:

单个文件的峰值传输内存 ≈ n × 分片大小n = max_parts_in_flight

放到整个客户端,内存要对同时在传的文件求和:

客户端峰值内存  ≈  max_upload_concurrency   × 上传 n × 上传分片大小
              +  max_download_concurrency × 下载 n × 下载分片大小

SDK 没有全局内存上限n 无限大内存就无限大,请务必控制。实用建议:

  • 分片大小保持在 1–8 MiB(对象存储常见区间)。太小则请求开销大;太大则每次重试要重发更多数据、且抬高内存下限。
  • n 从 4 开始试,大文件 + 快网时用。再往上调,等带宽跑满后收益很小,内存却翻倍。
  • 记得乘以文件级并发:4 个文件 × n=8 × 4 MiB = 最多 128 MiB 缓冲。

11. 错误处理与自动重试

大多数 SDK 调用返回 Result<_, MeowError>MeowError 上有两个关键方法:

  • MeowError::code():稳定的数字错误码(i32,适合结构化日志 / FFI);
  • MeowError::msg():给人看的错误描述。

分支判断请用 code(),不要去匹配错误文本。常见错误码:

名称什么时候发生
102ParameterEmpty入队时必填项(URL、文件名、非零大小)为空
103DuplicateTaskError同一个文件 / 任务已经在排队、运行或暂停
107ClientClosed客户端已关闭,请新建一个 MeowClient
111CommandSendFailed命令队列满了(快速失败),退避重试或调大 command_queue_capacity
120TaskCanceled任务在完成前被取消
121DiskFull写下载文件时磁盘满
122LocalFileRemoved传输过程中本地源 / 目标文件被删了

自动重试是怎么工作的

两个旋钮控制重试次数:

  • with_max_chunk_retries(n):单个分片首次失败后额外重试的次数(默认 30 关闭);
  • with_max_upload_prepare_retries(n):上传 prepare 阶段首次失败后额外重试的次数(默认 3)。

在这个预算内,SDK 只重试「瞬时(transient)传输错误」——连接被重置、超时、消息不完整、以及 HTTP 408/429/5xx——并在两次尝试之间用指数退避(基准 200ms、上限 5000ms)+ ±20% 抖动等待,避免重试风暴。非瞬时错误(如 403、格式错误的响应、无效 Range)快速失败,不消耗重试预算,因为重试也没用。


12. 常见问题排查表(FAQ)

现象可能原因解决
设了 n > 1 但还是串行三个前置条件没满足确认 n > 1、协议并发安全、且总大小已知(上传天然满足总大小)
上传没并发用的是内置默认 HTTP 上传 / 自定义协议挂上四种 provider 上传协议之一(direct / presigned / SAS)
进度卡住不动然后突然跳靠后的分片先传完、前面有空洞正常现象,进度按连续前缀单调推进,不是卡死
入队大量任务时报 CommandSendFailed命令队列满了(try_enqueue 快速失败)调大 command_queue_capacity,或自己退避重试
DuplicateTaskError同一个文件 / URL 的任务已经存在别重复提交;或先取消旧任务
关闭后再提交报 ClientClosed客户端 close() 之后不可复用新建一个 MeowClient
忘了调 close(),事件丢失 / 线程不退依赖了 Drop 兜底关机流程里显式 client.close().await

13. 附录:配置项速查表

MeowConfig(客户端级配置)

MeowConfig::default() 拿安全默认值,或用 MeowConfig::builder() 定制后 build()?。配置在客户端创建后不可变。

参数默认约束说明
max_upload_concurrency2>= 1同时上传的文件数
max_download_concurrency2>= 1同时下载的文件数
http_timeout5s正数内部构建 HTTP 客户端的单请求超时
tcp_keepalive30s正数TCP keepalive
command_queue_capacity128>= 1入队 / 暂停 / 恢复 / 取消 等命令队列容量
worker_event_queue_capacity256>= 1进度 / 状态事件队列容量
http_clientreqwest::Client可注入自定义 HTTP 客户端(代理、TLS、默认头、埋点等)

UploadPounceBuilder(上传任务)

方法必填?说明
new(file_name, file_path, chunk_size)文件来源的上传任务,chunk_size == 0 归一化为默认值
from_bytes(file_name, bytes, chunk_size)二选一内存字节来源
with_url(url)通常要上传目标 URL
with_breakpoint_upload(Arc::new(...))云上传要挂上 provider / 自定义上传协议
with_max_parts_in_flight(n)可选单文件分片并发数,默认 1;仅对并发安全协议生效
with_max_chunk_retries(n)可选单片重试次数,默认 3
with_max_upload_prepare_retries(n)可选prepare 阶段重试次数,默认 3
with_method(method)可选默认 / 自定义上传请求的 HTTP 方法(默认 POST
with_headers(headers)可选替换基础请求头
build()读文件元数据并返回 PounceTask,可能返回 std::io::Error

MeowClient(客户端)常用方法

方法用途
MeowClient::new(config)创建客户端(执行器懒启动)
enqueue_and_wait(task, progress_cb).await提交并等待到终态,返回 TaskOutcome
try_enqueue(task, progress_cb, complete_cb).await提交任务,返回 TaskId(快速失败,不等完成)
try_enqueue_paused(task, ..).await以暂停态导入,不做任何 IO,之后 resume 才启动
pause / resume / cancel (task_id).await暂停 / 恢复 / 取消
register_global_progress_listener(cb)注册观测「所有任务」进度的全局监听器
snapshot().await查看排队 / 活跃的任务组数量
close().await关机必调:优雅关闭

相关阅读

本文基于 rusty-cat 0.2.4 编写,所有代码片段均经过 cargo check 编译验证。