offload

package
v0.31.0 Latest Latest
Warning

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

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

Documentation

Overview

媒体类型 → 落盘扩展名的**单源**推导。

## 为什么内核要管这件事

Ext 由调用方逐条给,若不给推导,N 个宿主就会写 N 套映射,彼此不一致。 而落盘键是内容寻址的(见 StorePath):扩展名一变,同一份内容的键就变, 占位文本逐字节稳定性与去重**同时失效**(见 placeholder.go 文件注释)。 换句话说,这不是风格问题,是键一致性问题,故必须单源。

## 为什么不能只靠 mime.ExtensionsByType

两层不可移植性,每层都足以让同一份内容在不同机器上落进不同的键:

  1. 表内容依平台而变。Go 会加载宿主机的 mime.types(macOS 的 /etc/apache2/mime.types、多数发行版的 /etc/mime.types),同一类型在 A 机器注册 .jpg/.jpe/.jpeg、在 B 机器只有 .jpeg;
  2. 候选顺序依实现而变。平台表对同一类型可能返回多个扩展名,未显式定序时 「取哪个」在不同实现/版本间会漂。

故口径固定为三段:**剥参数 → 查显式覆盖表 → 平台表兜底**,兜底结果排序取首个, 使同一进程内反复调用恒等。常见类型由覆盖表兜住,第 1、2 条都碰不到它们。

## 未知类型为什么给中性值而不是报错

扩展名在本包只用于拼逻辑键,取值域是宿主的落盘实现,不参与内容语义判定。 让每个宿主自己判一次「这算不算错误」,只会得到 N 种失败方式;而报错会连带 丢掉「阈值已过、本可省上下文」的外置收益,中性取值则至少键稳定、行为可预期。

Package offload 内联大对象外置:把躺在消息历史里的二进制本体换成占位文本。

## 为什么需要这个包

大对象(base64 图片/音频/归档)随消息进历史后,会在**每一轮**被原样重发。 代价与对象大小成正比、与轮数相乘,而模型在该对象上真正需要的信息量通常只有 一次——第一轮看完之后,剩下的 N-1 份纯属浪费。本包把本体外置为内容寻址的落盘对象, 历史里只留一段占位文本,模型需要时按路径取回。

与 spill 是**姊妹机制而非重复**:spill 管「输出超预算被截断」(文本尾部溢出), offload 管「二进制本体躺在消息里」(输入侧上下文计量)。二者判定时机、 触发条件、替换语义都不同,故不合并。

## 关键取舍一:阈值——小对象外置必亏

占位文本本身有体量(缺省形态 191 字节,见 placeholder.go)。对一个小对象, 「写占位 + 多一次取回往返」的总成本**高于**直接内联,外置反而是负优化。 故低于 MinBytes 的对象一律原样保留;阈值取 4 KiB 远高于占位体量, 保证跨过阈值后收益一定为正。

## 关键取舍二:两阶段——首次绝不替换(正确性核心)

首次见到某对象时:落盘 + 标记,**但内容原地保留**。只有后续轮才换成占位文本。

理由只有一个,但足够致命:若首次就替换,模型这一轮**从未真正看到过该对象**, 后续轮的占位文本又把它指向一个「模型没看过的东西」——等于凭空丢失输入。 两阶段让「至少完整出现过一次」成为硬保证,压缩只发生在安全的那一侧。

连带一条:首次落盘失败时**不得标记**。否则下一轮会放出一个指向从未写入的路径的 占位文本(悬空引用),比不外置更糟。

连带二条:**标记按轮提交,不按条目提交**。「首次」的判据只能是「进入本轮时 已可见的标记」。若在轮内循环里就写 State,同一份内容出现两次时,第二份会被 自己刚写的标记判成「后续轮」而当场替换——那一轮模型从未完整看到过其中任何一份, 恰恰是上面那条保证要禁止的动作。参考实现逐块独立处理(标记挂在块自身上), 同一消息里两份相同图片都保持内联,本包与之对齐。

## 关键取舍三:状态归调用方

「已见过哪些摘要」必须跨轮存活,但**不属于本包**:它随会话存储、随宿主重启策略 变化。本包除调用方传入的 *State 与 Store 外无任何状态,纯函数式, 故可被任意编排层并发复用而不串扰。

## 落盘面是接缝不是实现

内核不碰存储布局(与 checkpoint.Store、contract 同一纪律):Store 由宿主实现, 本包只推导一个**逻辑键** StorePath 并交给它落盘。宿主须保证在 State 存活期内 该键可取回——本包不负责保留期与清理(spill 侧有 Prune,offload 侧的回收由宿主决定)。

占位文本契约:外置对象在消息历史里留下的那一小段替身文本。

## 为什么要有格式与解析两件套

占位文本是**跨轮复用的字符串**:内容寻址保证同一份内容逐轮得到逐字节相同的 占位文本,历史前缀因此能命中提示缓存——这正是外置收益的一半(另一半是不重发本体)。 一旦它可被机械解析,宿主才能在不重新计算摘要的前提下回答「这条历史上外置过什么」 (对账、回收、审计)。故本文件与 spill 的 TruncHint/ParseHint 同纪律:格式单源, 改格式即改契约。

## 字段取舍

每个字段都有存在理由,缺一个就会有一类问题变成不可排查:

  • kind/id:模型据此判断「这是什么东西、同一 id 是否同一份字节」;
  • path:取回入口。**必须与真实落盘键逐字一致**,否则模型按图索骥必然落空;
  • bytes:模型据此估算取回成本,决定要不要取;
  • mime:决定取回后如何解释;缺失时模型只能盲解。

解析面要求各字段不含空格(path 已由 StorePath 净化保证),这是格式契约的一部分。

Index

Constants

View Source
const (

	// DefaultRetrieveHint 缺省取回指引。
	//
	// 指引不写死具体工具名:取回入口由宿主决定(本地文件、对象存储、专用工具……),
	// 内核写死某个名字只会在宿主换实现后变成误导——指向一个不存在的工具
	// 比不给指引更糟。宿主有自定义时经 Request.RetrieveHint 覆盖。
	DefaultRetrieveHint = "原文不在本条消息内;需要时按 path 取回(id 相同即字节相同)。"
)
View Source
const FallbackExt = ".bin"

FallbackExt 未知(或无法解析的)媒体类型的缺省扩展名。

取 .bin 而非留空:留空会被 sanitizeExt 判为非法扩展名,条目级失败后内容内联, 白白越过阈值却不省任何上下文(Item.Reason 只够留痕,不够自愈)。

View Source
const MinBytes = 4096

MinBytes 外置阈值(缺省 4 KiB)。低于此尺寸的对象原样内联——占位文本的体量 加上一次取回往返的成本高于直接内联,小对象外置是负优化。 Request.MinBytes 可逐调用覆盖(≤0 时取本缺省)。

Variables

View Source
var (
	// ErrNoStore 未装配落盘面(Request.Store 为 nil),无从外置。
	ErrNoStore = errors.New("未装配外置落盘面,无法外置")

	// ErrBadExt 扩展名非法。扩展名会拼进逻辑键,必须先净化——
	// 未经净化的扩展名可以让同一份内容落进不同键,使去重与占位复用同时失效。
	ErrBadExt = errors.New("外置扩展名非法(只允许字母数字与短横线/下划线)")

	// ErrBadHash 摘要长度不足,无法推导分片目录。
	ErrBadHash = errors.New("内容摘要非法(至少 2 位十六进制)")
)

派生哨兵。均为**条目级**失败:触发时该条目保持原内容内联,不影响本轮其余条目。

Functions

func ExtForMimeType

func ExtForMimeType(mimeType string) string

ExtForMimeType 由媒体类型推导落盘扩展名,返回带前导点的形态(如 ".png"), 可直接填入 Blob.Ext;StorePath 两种形态都接受。

三段口径:剥参数(`image/jpeg; charset=x` → `image/jpeg`)→ 查覆盖表 → 查平台表(排序取首个)→ 未知返回 FallbackExt。参数与大小写都要剥:覆盖表以 裸类型为键,而带参数的写法在真实消息里极常见(不剥则整条覆盖形同虚设)。

刻意不返回 error:推导失败不是本包的失败面,本包没有「类型不对」这种错误类型, 只有一个必须恒定的中性取值(见 FallbackExt)。

func Render

func Render(r Ref, hint string) string

Render 生成占位文本。

缺省形态 191 字节(由 TestPlaceholderBodySmallerThanThreshold 钉住上限)——这是 MinBytes 取 4 KiB 的前提:小到占位体量的若干分之一, 跨过阈值后替换一定是净赚(见 offload.go 包注释「关键取舍一」)。 同一 (Ref, hint) 必须产出**逐字节相同**的文本,故渲染内不含时间戳、序号、 随机数等任何随轮变化的成分。

func StorePath

func StorePath(hash, ext string) (string, error)

StorePath 由内容摘要与扩展名推导逻辑落盘键:blobs/<前两位>/<摘要><扩展名>。

纯函数,且**同一 (hash, ext) 恒得同一键**——这是去重的全部机制:键一致, 占位文本才可能逐字节一致,历史前缀才能命中提示缓存。按摘要前两位分片只是 避免单目录堆积,宿主可忽略目录层级自行映射。

返回的键以 path 分隔(与宿主无关的逻辑键,不是本机文件路径)。

func TruncatedHash

func TruncatedHash(hash string) string

TruncatedHash 取占位文本里暴露的摘要前缀。

为什么只暴露前缀:全量摘要进了消息就等于给内容做了一次可跨库比对的指纹, 没必要让每一轮都带它在上下文里飘;前缀够用于人眼定位与占位文本复用。

Types

type Blob

type Blob struct {
	// Kind 对象类型(如 image/attachment)。参与占位文本,同一对象跨轮须给同一值,
	// 否则占位文本不可复用、缓存不命中。
	Kind string

	// MimeType 媒体类型;空则占位文本省略该字段。
	MimeType string

	// Ext 落盘扩展名(调用方给,内核不猜——猜错会写出一个后端解不开的键)。
	// 不给时用 ExtForMimeType(b.MimeType) 推导:那是本包的单源实现,宿主各写
	// 一套映射会让同一份内容在不同宿主上落进不同的键(见 mimeext.go)。
	Ext string

	// Data 原始字节。
	Data []byte
}

Blob 一条待处置的内联对象。

type Item

type Item struct {
	// Index 在 Request.Blobs 中的下标,调用方据此回填消息。
	Index int

	// Kind / Hash / Path 该对象的类型、全量摘要、逻辑落盘键。
	Kind string
	Hash string
	Path string

	// Bytes 原始字节数。
	Bytes int

	// Offloaded 该对象已有落盘副本(本次写的或此前写的)。
	Offloaded bool

	// Replaced 是否已用占位文本替换内容。
	//
	// **仅在后续轮为真**:首次见到时恒为 false,内容必须原地保留
	// (见包注释「关键取舍二」)。把它做成可判定的字段而不是隐含约定,
	// 是为了让「首轮替换」这类错误在测试里直接暴露。
	Replaced bool

	// Placeholder 占位文本;仅在 Replaced 为真时非空。
	Placeholder string

	// Reason 未处置/保持内联的原因(空=按预期处置);仅供留痕,不可据以重试。
	Reason string
}

Item 单条对象的处置结果。

type Ref

type Ref struct {
	// Kind 对象类型。
	Kind string

	// ID 摘要前缀(见 TruncatedHash)。
	ID string

	// Path 逻辑落盘键,须与实际落盘键逐字一致。
	Path string

	// Bytes 原始字节数。
	Bytes int

	// MimeType 媒体类型;空则不渲染该字段。
	MimeType string
}

Ref 占位文本的可解析部分(取回指引不在契约内,由宿主自定故不解析)。

func Parse

func Parse(text string) (Ref, bool)

Parse 解析占位文本:命中定长前缀且字段齐全才返回 true,否则返回 (零值, false)。

反向的宽松度要求与写入侧对称:宁可漏判(当作普通文本)也不误判—— 把一段普通文本错认成外置占位会让宿主去取一个并不存在的键。

type Report

type Report struct {
	Items []Item

	// Stored 本次实际落盘的条数(去重的直接证据:同内容第二轮不再写)。
	Stored int

	// Replaced 本轮替换为占位文本的条数。
	Replaced int

	// Inline 保持原内容内联的条数(含低于阈值与条目级失败)。
	Inline int
}

Report 一轮的外置报告(条目数与 Blobs 对齐)。

func Apply

func Apply(req Request) Report

Apply 处置一轮的内联对象。

签名**不返回 error**——这是刻意的:条目级失败在机制上就无法中断本轮, 调用方不存在「忘了处理 err 于是整轮报废」的可能。失败一律落到 Item.Reason 与 Report.Inline 上,内容原样保留。

type Request

type Request struct {
	// Blobs 本轮内联的对象(按调用方顺序处理,互不影响)。
	Blobs []Blob

	// State 跨轮状态;nil 时每轮都按首次处理(内容始终完整,压缩不生效)。
	State *State

	// Store 落盘面;nil 时全部条目保持内联并给出 ErrNoStore 留痕。
	Store Store

	// MinBytes 覆盖缺省阈值;≤0 取 MinBytes。
	MinBytes int

	// RetrieveHint 占位文本里的取回指引(宿主自述其取回方式);空用缺省指引。
	RetrieveHint string
}

Request 一轮的外置请求。

type State

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

State 已外置对象的摘要集合(**调用方持有**,须跨轮存活以兑现第二轮起的压缩)。

零值与 nil 均可用:nil 视作「什么都没见过」,后果是每轮都按首次处理—— 内容永远完整可见、只是省不下上下文。保守方向,永不丢输入。

并发安全:编排可能从多个环节并发推进,共享同一个 State 是常态而非意外。 未经保护的 map 在并发读写下是**不可 recover 的致命错误**,故这里加锁, 与 checkpoint.SeqGate / budget.Meter / tooldisclose.Catalog 同一口径。

func NewState

func NewState() *State

NewState 构造空状态。

func (*State) Len

func (s *State) Len() int

Len 已记入的摘要数。

func (*State) Mark

func (s *State) Mark(hash string)

Mark 记入摘要。调用方须仅在**落盘成功后**调用(见包注释「关键取舍二」的连带约束)。

func (*State) Seen

func (s *State) Seen(hash string) bool

Seen 报告该摘要此前是否已外置(两阶段判据)。

type Store

type Store interface {
	Put(relPath string, data []byte) error
}

Store 外置落盘边界(唯一接缝;宿主实现)。

实现须保证:

  • Put 对同一 relPath 重复调用**幂等**(内容寻址下重复写必然是同一份字节);
  • relPath 是逻辑键而非本机路径,宿主自行决定落点;
  • 在 State 存活期内该键可取回(保留期与回收由宿主负责,本包不清理)。

Jump to

Keyboard shortcuts

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