resumable

package
v0.44.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 10, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package resumable 提供「分片上传/断点续传」的会话生命周期状态机原语: 会话创建(含秒传钩子)、分片登记、断点查询、完成、中止、孤儿清扫, 状态面纯内存、存储后端接口化(分片字节落哪由 ChunkStore 决定), stdlib-only(pkg 零第三方依赖红线)。

为什么需要它

下游 zeus 两个仓**各自独立**手搓了整套同构状态机(media 主实现 ~700 行 + gin-vue-admin 又一份 ~400 行,pkg#361):会话建立/分片幂等/断点对账/ 合并抢占/孤儿清扫每个上传服务都要面对,而并发正确性的坑——CAS 进度、 合并抢占、missing 对账、孤儿清扫——是必须单点的语义,两份实现的防御 注释里各自都踩过这四个坑。业务差异其实只在**去重键**(DedupResolver) 与**合并落存储**(MergeFunc)两处,其余全是共同面。

状态机

StatusUploading --ClaimForMerge--> StatusMerging --Finish--> StatusDone
StatusUploading --Abandon--> StatusAborted
StatusMerging   --ReleaseClaim--> StatusUploading(校验/合并失败回滚)

四类并发坑的单点兑现(SessionStore 契约 + Manager 编排):

  • **CAS 进度**:Manager.PutChunk 幂等分片 + 写后存活确认 + AppendReceived 原子登记(只在收片态、重复登记幂等);
  • **合并抢占**:Manager.Complete 的 ClaimForMerge 是 0→1 原子迁移, 并发 Complete 恰好一个赢家进合并、输家拿 ErrMergeInProgress;
  • **断点对账**:Manager.ListParts 双源取交(「已登记 ∧ 字节存活 ∧ 字节数吻合」才算已收)——登记了但字节丢/残缺的分片必须回缺片清单, 否则断点查询会把客户端骗去 Complete 才炸;
  • **孤儿清扫**:Manager.Sweep 单点收口过期会话/无主分片/合并卡死 (过期判定走 UpdatedAt——收片与状态迁移都会续期)。

用法

sessions := resumable.NewMemSessionStore()      // 状态面:纯内存
chunks := store{...}                            // 存储面:实现 ChunkStore
m := resumable.NewManager(sessions, chunks)

s, uploaded, err := m.Init(ctx, resumable.InitReq{
    Owner: uid, Hash: h, Size: size, ChunkSize: 8 << 20,
}, func(ctx context.Context, hash string) (bool, error) {
    return objectExists(ctx, hash)               // 秒传:内容已存在
})
// uploaded=true ⇒ 秒传命中,直接复用既有对象

err = m.PutChunk(ctx, s.ID, idx, r, 8<<20)      // 幂等分片 + 进度 CAS
recv, missing, st, _ := m.ListParts(ctx, s.ID)  // 断点查询(双清单对账)
err = m.Complete(ctx, s.ID, func(ctx context.Context, s *resumable.Session, r io.Reader) error {
    return putObject(ctx, s.Hash, r)             // 合并落存储(流式)
})
err = m.Abort(ctx, s.ID, uid)                   // 归属校验 + 清分片
n, err := m.Sweep(ctx, 24*time.Hour)            // 孤儿清扫

边界(如实声明,不伪装全知)

  • **合并的幂等性**:merge 成功而 Finish 失败时会话留在合并态、由 Sweep 收口(不重跑 merge);merge 失败则回滚回收片态、调用方重试 Complete 会**再跑一次** merge——落存储的写入要么幂等要么自持事务;
  • **超长分片的字节**:PutChunk 流式不过缓冲,超长判定发生在读满 want+1 之后——存储里可能短暂留下残片(不登记、对账算 missing), 重传覆盖或 Abort/Sweep 清掉;
  • **here 式的业务语义不内置**:秒传的「已存在」判定、合并的目标布局 (单对象/分块对象/去重库)、留存策略(完成即删 vs 定期删)都是钩子/ 调用方的事;Sweep 按「过期即清」的单一口径走;
  • **存储后端的一致性**:本包假定 ChunkStore 的 Put 是「以写代换」、 Stat 是最终一致的字节数;最终一致存储上「写后存活确认」可能把 读旧值判成残缺(ErrChunkSize)——分片幂等重传即可修复,不影响 状态机正确性。

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrSessionNotFound 会话不存在。
	ErrSessionNotFound = errors.New("resumable: 会话不存在")
	// ErrSessionExists 会话 id 已存在(Create 撞 id)。
	ErrSessionExists = errors.New("resumable: 会话已存在")
	// ErrChunkNotFound 分片不存在(Stat/Open 未命中)。
	ErrChunkNotFound = errors.New("resumable: 分片不存在")
	// ErrSessionDone 会话已完成——完成态不可再收片/合并/中止。
	ErrSessionDone = errors.New("resumable: 会话已完成")
	// ErrSessionAborted 会话已中止。
	ErrSessionAborted = errors.New("resumable: 会话已中止")
	// ErrMergeInProgress 合并进行中:合并抢占的输家、或对合并中的会话
	// 收片/中止。输家可轮询 [Manager.ListParts] 的状态等赢家收场。
	ErrMergeInProgress = errors.New("resumable: 合并进行中")
	// ErrNotOwner 归属不符:Abort 的 owner 与会话归属不一致。
	ErrNotOwner = errors.New("resumable: 归属不符")
	// ErrBadChunkIndex 分片序号越界(不在 [0, NumChunks) 内)。
	ErrBadChunkIndex = errors.New("resumable: 分片序号越界")
	// ErrChunkSize 分片字节数与会话数学不符(偏短/残缺)。
	ErrChunkSize = errors.New("resumable: 分片字节数不符")
	// ErrChunkTooLarge 分片字节数超过上限(会话分片大小或调用方 maxChunk)。
	ErrChunkTooLarge = errors.New("resumable: 分片字节数超过上限")
	// ErrMissingChunks 完成时分片不齐(收到的缺清单见包装上下文)。
	ErrMissingChunks = errors.New("resumable: 分片不齐")
	// ErrBadInit 会话参数非法(Hash 空、Size 负、ChunkSize 非正等)。
	ErrBadInit = errors.New("resumable: 会话参数非法")
)

状态机与存储面的哨兵错误(errors.Is 可判)。

Functions

This section is empty.

Types

type ChunkStore

type ChunkStore interface {
	Put(ctx context.Context, id string, idx int, r io.Reader) error
	Stat(ctx context.Context, id string, idx int) (int64, error)
	Open(ctx context.Context, id string, idx int) (io.ReadCloser, error)
	DeletePrefix(ctx context.Context, id string) error
}

ChunkStore 是分片字节的存储后端(接口化:本地盘、对象存储、内存皆可)。 实现契约:

  • Put 以写代换(同 id+idx 整体覆盖,不追加);
  • Stat 回已写字节数,缺失报 ErrChunkNotFound(实现的读侧错误经 errors.Is 可判这个哨兵,其他错误原样上抛——「读不到」与「没有」 必须可分);
  • Open 回已写字节的读侧(缺失报 ErrChunkNotFound),调用方负责 Close;
  • DeletePrefix 删掉该会话的全部分片,**幂等**(没有即成功)—— 孤儿清扫靠它收口。

type DedupResolver

type DedupResolver func(ctx context.Context, hash string) (bool, error)

DedupResolver 是秒传钩子:内容摘要已存在(对象已完整落库)返回 true, Init 即回 (nil, true, nil)——不再建会话、不再收片。业务差异在去重键语义 (按 hash、按 hash+size、按租户隔离……),所以做成钩子而不是内置规则。

type InitReq

type InitReq struct {
	// ID 会话 id;空则由 [Manager.Init] 生成(crypto/rand 16 字节 hex)。
	ID string
	// Owner 归属键(必填语义由调用方定,Abort/Sweep 靠它分清「谁的会话」)。
	Owner string
	// Hash 全量内容摘要——秒传钩子的去重键(必填非空)。
	Hash string
	// Size 全量字节数(>= 0)。
	Size int64
	// ChunkSize 分片大小(> 0)。
	ChunkSize int64
}

InitReq 建会话的请求。

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager 把状态机与两类存储串成上传会话面:会话创建、分片登记、断点 查询、完成、中止、孤儿清扫。构造后并发安全(互斥在两个 store 里)。

func NewManager

func NewManager(sessions SessionStore, chunks ChunkStore) *Manager

NewManager 构造 Manager。任一 store 为 nil 立即 panic(仓规总则①: 构造期非法值 fail-fast——nil store 到第一个调用点才炸就晚了)。

func (*Manager) Abort

func (m *Manager) Abort(ctx context.Context, id string, owner string) error

Abort 中止会话(归属校验):校验 owner 后把会话打到 StatusAborted 并清掉全部分片。StatusAborted 态再来是幂等收口(再清一次分片); StatusDone 不可中止(ErrSessionDone);合并中不可中止 (ErrMergeInProgress——等赢家收场再决定删不删)。

func (*Manager) Complete

func (m *Manager) Complete(ctx context.Context, id string, merge MergeFunc) error

Complete 完成会话:合并抢占 → 齐全校验 → merge 交付 → 收尾清分片。

  • 合并抢占(0→1 原子):并发 Complete 恰好一个赢家进合并,输家拿 ErrMergeInProgress(可轮询等赢家收场);StatusDone 态再来是 幂等 nil,StatusAborted 态报 ErrSessionAborted;
  • 齐全校验:逐片「已登记 ∧ 字节存活 ∧ 字节数吻合」双源对账,缺一片 都不合并——回滚回收片态(ReleaseClaim)并报 ErrMissingChunks, 客户端补传后可直接重试(会话不换);
  • merge 失败同样回滚回收片态(瞬态故障重试不丢已传分片);
  • merge 成功即 Finish(StatusDone)并删分片(合并结果已交付业务 存储,分片留着就是孤儿字节);Finish 失败时会话留在合并态由 Manager.Sweep 收口(merge 已交付,不重跑)。

func (*Manager) Init

func (m *Manager) Init(ctx context.Context, req InitReq, dedup DedupResolver) (*Session, bool, error)

Init 建会话(或命中秒传):

dedup 命中 → (nil, true, nil):内容已存在,调用方直接复用既有对象;
否则按 req 建会话(StatusUploading)并返回 (会话, false, nil)。

req.ID 空则生成;Hash 空、Size<0、ChunkSize<=0 报 ErrBadInit。

func (*Manager) ListParts

func (m *Manager) ListParts(ctx context.Context, id string) (received, missing []int, status Status, err error)

ListParts 断点查询(续传对账):返回已收/缺片双清单与会话状态。

对账口径是**双源取交**:「已登记 ∧ 字节存活 ∧ 字节数吻合」才算已收—— 登记了但字节丢/残缺的分片必须回缺片清单(客户端重传),登记了却拿去 断点续传会在 Complete 才炸、把断点查询变成谎言。读不到(存储故障)不 归为缺片:错误原样上抛,不给客户端「重传能修好」的假答案。

func (*Manager) PutChunk

func (m *Manager) PutChunk(ctx context.Context, id string, idx int, r io.Reader, maxChunk int64) error

PutChunk 登记一片(幂等分片 + CAS 进度 + 写后存活确认):

  • 只收 StatusUploading 态的会话(合并中/完成/中止各有各的哨兵);
  • idx 必须落在 [0, NumChunks),否则 ErrBadChunkIndex;
  • 字节数必须**恰好**是会话数学给的应有长度(非末片 = ChunkSize、 末片 = 余数):超长报 ErrChunkTooLarge、偏短/残缺报 ErrChunkSize ——宁可让客户端重传一片,也不让「大小不对的分片」流进合并;
  • maxChunk 是调用方防线(客户端声称的分片大小不可信时的护栏): 低于本片应有长度直接报 ErrChunkTooLarge,读取上限取会话数学;
  • 写后存活确认:Put 之后 Stat 复核字节数才登记进度——「写了」不等于 「写住了」,断点对账把「登记了但字节丢了」的分片算进 missing。

func (*Manager) Sweep

func (m *Manager) Sweep(ctx context.Context, olderThan time.Duration) (int, error)

Sweep 孤儿清扫的单点语义:把 UpdatedAt 早于 now-olderThan 的会话整批 收口——非终态先 Abandon、分片全删、记录删除(过期会话/无主分片/合并 卡死都归它)。返回成功清扫的会话数;失败的继续清扫、错误用 errors.Join 聚合上抛(清扫不因单个会话失败而中断)。olderThan < 0 报错。

type MemChunkStore

type MemChunkStore struct {
	// contains filtered or unexported fields
}

MemChunkStore 是 ChunkStore 的内存实现(并发安全)。分片整块驻留 内存——流式上限由 Manager.PutChunk 的会话数学+maxChunk 把门, 直接拿本类型当裸存储用的调用方自限。同 id+idx 以写代换;Open 返回的 读器基于不可变字节块,写覆盖不干扰进行中的读。

func NewMemChunkStore

func NewMemChunkStore() *MemChunkStore

NewMemChunkStore 构造空的内存分片存储。

func (*MemChunkStore) DeletePrefix

func (m *MemChunkStore) DeletePrefix(ctx context.Context, id string) error

DeletePrefix 删掉会话全部分片;没有即成功(幂等——孤儿清扫靠它收口)。

func (*MemChunkStore) Open

func (m *MemChunkStore) Open(ctx context.Context, id string, idx int) (io.ReadCloser, error)

Open 回分片读器;缺失报 ErrChunkNotFound。读器基于写入时的不可变 块(覆盖写生成新块,进行中的读不受影响)。

func (*MemChunkStore) Put

func (m *MemChunkStore) Put(ctx context.Context, id string, idx int, r io.Reader) error

Put 以写代换存一片(读尽 r)。

func (*MemChunkStore) Stat

func (m *MemChunkStore) Stat(ctx context.Context, id string, idx int) (int64, error)

Stat 回分片字节数;缺失报 ErrChunkNotFound。

type MemSessionStore

type MemSessionStore struct {
	// contains filtered or unexported fields
}

MemSessionStore 是 SessionStore 的内存实现(并发安全)。 CAS 进度 / 合并抢占 / 状态单向 / 读侧快照四条契约都在互斥里单点兑现。

func NewMemSessionStore

func NewMemSessionStore() *MemSessionStore

NewMemSessionStore 构造空的内存会话存储。

func (*MemSessionStore) Abandon

func (m *MemSessionStore) Abandon(ctx context.Context, id string) error

Abandon 中止:非终态 → StatusAborted;StatusAborted 幂等; StatusDone 不可中止(报错)。

func (*MemSessionStore) AppendReceived

func (m *MemSessionStore) AppendReceived(ctx context.Context, id string, idx []int) error

AppendReceived 登记分片进度(CAS 面):只在 StatusUploading 态收登记, 已登记序号幂等;其他状态按对应哨兵报错(登记与收片状态强绑定—— 合并中/完成后再登记就是状态机破口)。

func (*MemSessionStore) ClaimForMerge

func (m *MemSessionStore) ClaimForMerge(ctx context.Context, id string) (bool, error)

ClaimForMerge 原子抢占(0→1):StatusUploading → StatusMerging 回 true;其余状态回 false(幂等读,不报错——输家的语义就是「没抢到」)。

func (*MemSessionStore) Create

func (m *MemSessionStore) Create(ctx context.Context, s *Session) error

Create 登记新会话;id 重复报 ErrSessionExists。

func (*MemSessionStore) Delete

func (m *MemSessionStore) Delete(ctx context.Context, id string) error

Delete 删会话记录(含进度);缺失报 ErrSessionNotFound(清扫面把 「没删到」当真实失败,不吞)。

func (*MemSessionStore) Finish

func (m *MemSessionStore) Finish(ctx context.Context, id string) error

Finish 收场:StatusMerging → StatusDone;其余状态报错。

func (*MemSessionStore) Get

func (m *MemSessionStore) Get(ctx context.Context, id string) (*Session, error)

Get 回会话快照(深拷贝);缺失报 ErrSessionNotFound。

func (*MemSessionStore) List

func (m *MemSessionStore) List(ctx context.Context) ([]*Session, error)

List 回全部会话快照(按 id 升序,确定性输出)。

func (*MemSessionStore) ReleaseClaim

func (m *MemSessionStore) ReleaseClaim(ctx context.Context, id string) error

ReleaseClaim 回滚合并抢占:StatusMerging → StatusUploading;其余状态 报错(回滚只对「抢了没交」的会话有意义)。

type MergeFunc

type MergeFunc func(ctx context.Context, sess *Session, r io.Reader) error

MergeFunc 是合并落存储的钩子:r 为**全部分片按序号拼接**的字节流 (流式打开,不整对象进内存),把 r 写进业务存储即完成交付。合并成功后 Manager 负责 Finish 与分片回收;merge 返回 error 时会话回滚回收片态 (ReleaseClaim),调用方可修正后重试 Complete。业务差异只在落哪(对象 存储、文件、去重库……),并发正确性(谁来合并、缺片不合并、孤儿收口) 由状态机单点兜住。

type Session

type Session struct {
	// ID 会话 id(Init 生成或调用方给定)。
	ID string
	// Owner 归属键(调用方语义:用户 id、租户键……),Abort 按它校验。
	Owner string
	// Hash 全量内容摘要(去重键候选,秒传钩子的入参)。
	Hash string
	// Size 全量字节数(>= 0;0 表示空对象,无分片)。
	Size int64
	// ChunkSize 分片大小(> 0);末片可短于它。
	ChunkSize int64
	// Status 当前状态。
	Status Status
	// CreatedAt / UpdatedAt 会话建立 / 最后进展时刻(Sweep 的过期判据)。
	CreatedAt time.Time
	UpdatedAt time.Time
	// Received 是**已登记且存活确认过**的分片序号(升序快照)。
	// 断点对账的双源之一:状态是进度快照,字节是真值(见 ListParts)。
	Received []int
}

Session 是一个上传会话的事实面(SessionStore 的读侧快照——真值在 store,返回值是拷贝,改它不改 store)。

func (*Session) NumChunks

func (s *Session) NumChunks() int

NumChunks 返回分片总数(Size==0 时为 0)。

type SessionStore

type SessionStore interface {
	Create(ctx context.Context, s *Session) error
	Get(ctx context.Context, id string) (*Session, error)
	List(ctx context.Context) ([]*Session, error)
	AppendReceived(ctx context.Context, id string, idx []int) error
	ClaimForMerge(ctx context.Context, id string) (bool, error)
	ReleaseClaim(ctx context.Context, id string) error
	Finish(ctx context.Context, id string) error
	Abandon(ctx context.Context, id string) error
	Delete(ctx context.Context, id string) error
}

SessionStore 是会话状态机的存储后端。实现契约(并发正确性的四类坑都 压在这份接口上,实现必须单点兜住):

  • **CAS 进度**:AppendReceived 只在 StatusUploading 态登记、已登记 序号幂等;并发登记不丢、不重;
  • **合并抢占**:ClaimForMerge 的 Uploading→[StatusMerging] 是原子 0→1 迁移,恰好一个赢家(输家拿 false);
  • **状态单向**:Finish 只从 Merging→[StatusDone];ReleaseClaim 只从 Merging→Uploading(合并失败回滚,重试不换会话);Abandon 把非终态 打到 StatusAborted(Aborted 幂等,Done 不可中止);
  • **读侧快照**:Get/List 返回深拷贝,调用方改写不污染内部状态。

type Status

type Status int

Status 会话状态机的状态。状态迁移是单点语义(SessionStore 保证原子):

StatusUploading --ClaimForMerge--> StatusMerging --Finish--> StatusDone
StatusUploading --Abandon--> StatusAborted
StatusMerging   --ReleaseClaim--> StatusUploading(校验/合并失败回滚)
const (
	// StatusUploading 收片中:唯一可登记分片的状态。
	StatusUploading Status = iota
	// StatusMerging 合并中:已被 [SessionStore.ClaimForMerge] 抢占。
	StatusMerging
	// StatusDone 完成:合并已交付,终态。
	StatusDone
	// StatusAborted 中止:终态(可再清分片)。
	StatusAborted
)

func (Status) String

func (s Status) String() string

String 给日志/调试用。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL