Documentation
¶
Overview ¶
Package reliable 提供可靠消费的状态机内核与持久化抽象(opus5-RCC-v2 §1~§8)。
根包零第三方依赖(J2):只含纯契约类型与纯函数。gorm/数据库驱动/prometheus/gin 全部在 store 子树与消费服务侧。
关键不变量(由 store/repotest 在 MySQL/PostgreSQL 双方言上验证):
- 一张 event_consumption 表 = 幂等 + 死信 + 重投调度(M1)。
- 五状态单状态机:PROCESSING | SUCCEEDED | RETRY_SCHEDULED | DEAD_LETTER | DISCARDED(§3)。
- TryClaim 独立提交、构造期保证(NewStore 派生独立 session,§3.3);Mark* 显式接收 *gorm.DB(M14)。
- claim_id(ClaimToken)校验让 0 行从「设计边界」变成「可判定异常」(M3/edge #6/#6b)。
Index ¶
- Constants
- Variables
- func AdvanceAttempt(current int) int
- func Backoff(attempt int, base, cap time.Duration, jitterFraction float64) time.Duration
- func CanAutoReplay(s ReplaySafety) bool
- func CanManualReplay(s ReplaySafety) bool
- func CanTransition(from, to Status) bool
- func IsPermanent(err error) bool
- func IsRetryable(err error) bool
- func IsTerminal(s Status) bool
- func Permanent(err error) error
- func Retryable(err error) error
- func SanitizeForLog(s string) string
- func SanitizeForStorage(s string) string
- func ShouldDeadLetter(attempt, maxAttempts int) bool
- type AggregateGateKey
- type Alerter
- type ClaimInput
- type ClaimToken
- type ConsumptionMetrics
- type Decision
- type DeliveryMeta
- type ErrorClass
- type ErrorClassifier
- type HandlerID
- type HeaderPair
- type Key
- type Meta
- type MetaProvider
- type NoOpAlerter
- type NoOpMetrics
- type PermanentError
- type ReplaySafety
- type ReplayableHandler
- type RetryableError
- type Status
- type TerminalOutcome
Constants ¶
const ( MetricDeadLetterTotal = "consumption_dead_letter_total" MetricRecordFailureTotal = "consumption_record_failure_total" MetricAnomalyTotal = "consumption_anomaly_total" MetricPartitionStalledSecs = "consumption_partition_stalled_seconds" MetricPendingCount = "consumption_pending_count" MetricReplayBlockedCount = "consumption_replay_blocked_count" MetricOutboxDeadLetteredTotal = "outbox_dead_lettered_total" )
指标名常量(§8.4/§10,M13 规范)。必须与 §10 告警表逐一对应。
const ( LabelHandler = "handler" LabelTenant = "tenant" LabelErrClass = "error_class" LabelKind = "kind" LabelStatus = "status" LabelTopic = "topic" LabelPartition = "partition" )
标签常量(M13)。
const ( DefaultBackoffBase = time.Second DefaultBackoffCap = time.Hour )
DefaultBackoffBase / DefaultBackoffCap(§6.2 起点 1s / 上限 1h)。
const DefaultMaxAttempts = 5
DefaultMaxAttempts 是 attempt 耗尽终点的默认上限(§6.2)。MarkFailed 的 ShouldDeadLetter 与 replay scheduler 的 ErrRetryLater 让路终点共用同一常量——失败路径与让路路径的「重试天花板」必须对称: 否则一个永远返回 ErrRetryLater 的 handler 会以 ~1h 间隔无限重试、永不进死信(scheduler.processOne 的 InvokeRetryLater 分支据此把 attempt≥max 升级为 DEAD_LETTER + REPLAY_DEFER_EXHAUSTED)。服务可按 handler 覆盖;PR-3 的装饰器应让 MarkFailed 与 scheduler 取同一值。
Variables ¶
var ErrConflict = errors.New("reliable: conflicting state")
ErrConflict 终态 CAS 版本不符(运维 API expected_row_version 不匹配)或 RecordTerminal 遇现存 PROCESSING, 或 ScheduleReplay 的 requester==approver(双人确认违规)。
var ErrDuplicateKey = errors.New("reliable: duplicate key violation")
ErrDuplicateKey 表示某次写入撞了唯一索引。它只声明事实,不声明该冲突是否等于幂等命中 —— handler 必须自行核实约束名属本次事件的幂等键、读回已有行、比对内容一致后,才能包成 ErrSkip(§5)。 取代 evidence-management 与 process-management 各自的 service.ErrDuplicateKey 分裂副本(PR1_SCOPE C5)。
var ErrIllegalTransition = errors.New("reliable: illegal status transition")
ErrIllegalTransition 由 store 在检测到非法转移时返回(纵深防御)。
var ErrNotPermitted = errors.New("reliable: replay not permitted")
ErrNotPermitted 重放时 CanAutoReplay=false 命中(纵深防御,正常不应触发;触发即 §6.1 矩阵有漏洞)。
var ErrNotSelfReplayable = errors.New("reliable: row not self-replayable")
ErrNotSelfReplayable 行的 payload IS NULL,靠 broker 重投;若该行已 ACK 则 broker 不再投,需人工介入。
var ErrRetryLater = errors.New("reliable: retry later")
ErrRetryLater 由 TryClaim 在 AlreadyProcessing 分支返回:他人持有有效租约,本次不落库、不 ACK, 交还 broker 稍后重投(§3.1)。
var ErrSkip = errors.New("reliable: idempotent skip")
ErrSkip 由 handler 在第 1 级显式声明:本次事件已被认定为幂等命中(如核实后的唯一冲突), 装饰器按成功终结(§4 Phase B 的 MarkSucceeded 分支)。第 2 级 driver classifier 不再兜底判 Skip。
Functions ¶
func AdvanceAttempt ¶
AdvanceAttempt 把 attempt 推进到下一次业务执行(RETRY_SCHEDULED→PROCESSING 时调用)。
func Backoff ¶
Backoff 计算下次重试的退避时长(§6.2)。纯函数:jitterFraction ∈ [0,1) 由调用方提供。 公式:base × 2^(attempt-1),封顶 cap;jitter ±20%。
func CanManualReplay ¶
func CanManualReplay(s ReplaySafety) bool
CanManualReplay 报告该 safety 是否允许人工重放(三类都允许;ReplayUnsafe 需额外双人确认)。
func Permanent ¶
Permanent 把 err 标记为 POISON(终态 DEAD_LETTER)。handler 对「外键关系不会最终一致」等 明确判断也可用它覆盖第 2 级 1452→Retryable 的默认(§5)。
func SanitizeForLog ¶ added in v1.7.4
SanitizeForLog is the log-line redaction entry point (F4-extend). The DLQ cause is scrubbed before it lands in a DB row, but the SAME cause — which can echo the tenant DSN — was otherwise rendered to logs verbatim (the sdk logger has no built-in redaction). Route every cause/err rendered into a reliable- path log line through this. It shares the storage scrubber so the log and storage paths can never diverge below the D11 floor; the two entry points are distinct only so callers express intent and so a future lighter-touch log form can be introduced without touching storage callers.
func SanitizeForStorage ¶ added in v1.7.4
SanitizeForStorage scrubs secrets and truncates to 2KB on a rune boundary (spec §10). The ordering is truncate → regex → truncate:
- The PRE-truncate bounds the regex input. The DLQ cause is untrusted error text (a GORM/driver message can itself echo attacker bytes); capping it first keeps the regex engine linear even if a future pattern reintroduces backtracking.
- The POST-truncate re-enforces the 2KB storage ceiling because redaction can GROW the string ([REDACTED] is longer than a short match).
The rune-boundary backoff (C5) is preserved at both truncation sites: a naive s[:2048] splits a multi-byte character in half and produces invalid UTF-8, which the utf8mb4 column then rejects (MySQL error 1366) or silently replaces with U+FFFD. CJK error messages routinely hit this.
This is the canonical root scrubber. The DLQ adapter (adapters/eventbus) and the service-side EventBusDLQAdapter both route quarantine error text through it; the kernel's own MarkFailed/RecordTerminal paths still call the internal store/gormshared.sanitizeMsg (unchanged — additive PR, no delegation wired).
func ShouldDeadLetter ¶
ShouldDeadLetter 报告「已开始的业务执行次数」是否已达上限。
Types ¶
type AggregateGateKey ¶
AggregateGateKey 是 DB aggregate lease 的身份(§6.2.1)。同 key 串行,不同 key 并行。
func (AggregateGateKey) Empty ¶
func (a AggregateGateKey) Empty() bool
Empty 报告聚合身份是否为空(无聚合的通知类事件跳过 gate)。
type Alerter ¶
type Alerter interface {
AlertPoison(handlerID HandlerID, eventID string, cause error)
AlertRecordFailure(handlerID HandlerID, cause error)
AlertAnomaly(kind string, handlerID HandlerID, detail string)
}
Alerter 把「需人工立刻看」的事件推给服务侧告警通道(§10 P1)。
type ClaimInput ¶
type ClaimInput struct {
Key Key
Meta Meta
TenantID int
// Delivery 是 eventbus.RawMeta 的 kernel 侧投影(避免 import eventbus,从而不把 sarama 带进 kernel)。
Delivery DeliveryMeta
}
ClaimInput 是 TryClaim 一次取得 Key、业务 Meta、tenant 与 broker RawMeta 的入参(§3.1 v2.7)。
type ClaimToken ¶
type ClaimToken string
ClaimToken 是占位的「票据」(fencing token)。**对调用方是不透明字符串**:禁止解析、拆分或假定其内部结构 (PR-3 adapter 只透传、比较)。MarkSucceeded/MarkFailed/MoveToDeadLetterWithToken 凭它做 WHERE claim_id = ? 校验(§3.1);持令牌者丢失所有权(租约被回收)时命中 0 行 → reliable.ErrConflict(review #17)。
注意:AcquireAggregateGate 返回的 gate token(string)是另一种格式(holder+uuid),不可与 ClaimToken 互换。
type ConsumptionMetrics ¶
type ConsumptionMetrics interface {
IncDeadLetter(handlerID HandlerID, class ErrorClass)
IncRecordFailure(handlerID HandlerID)
IncAnomaly(kind string, handlerID HandlerID)
IncReplayBlocked(handlerID HandlerID)
SetPending(status Status, handlerID HandlerID, tenantID int, n int64)
}
ConsumptionMetrics 由服务用自有 prometheus registry 实现(core 只定义接口,J2 不引 prometheus)。
type DeliveryMeta ¶
type DeliveryMeta struct {
Topic string
Partition int32
Offset int64
BrokerTimestamp time.Time
PayloadHash string
RawKey []byte
Headers []HeaderPair
}
DeliveryMeta 是 RawMeta 的 kernel 侧投影。由消费服务从 eventbus.RawMeta 映射。
type ErrorClass ¶
type ErrorClass string
ErrorClass 是错误分类的结果(§5)。
const ( // ClassRetryable 瞬态:DB 死锁/锁等待/超时/连接限/关停中/1452 缺父(默认)。 ClassRetryable ErrorClass = "RETRYABLE" // ClassPoison 永不可恢复(消息结构损坏或永不变的业务规则)。终点 DEAD_LETTER(§5 v2.6)。 ClassPoison ErrorClass = "POISON" // ClassUnrecoverable 兜底 + schema 漂移。 ClassUnrecoverable ErrorClass = "UNRECOVERABLE" // ClassConflict 未被第 1 级认领的唯一冲突(1062/23505 未核实为幂等命中)。终点 DEAD_LETTER。 ClassConflict ErrorClass = "CONFLICT" // ClassSkip 仅由 handler 第 1 级显式声明(ErrSkip);第 2 级不兜底判 Skip。终点按成功终结。 ClassSkip ErrorClass = "SKIP" )
func Classify ¶
func Classify(err error, driver ErrorClassifier) ErrorClass
Classify 是两级分类的纯函数(§5):
- 领域显式声明(权威):PermanentError→Poison;RetryableError→Retryable;ErrSkip→Skip。
- 基础设施错误码(精确,保守默认):driver 识别 + context 超时/取消 + net 超时。
- 兜底 Unrecoverable(v2.5:未知不再当 Retryable)。
type ErrorClassifier ¶
type ErrorClassifier interface {
// ClassifyDriver 只做保守默认;返回 (class, true) 表示命中已知驱动错误码,(0, false) 表示不认识。
ClassifyDriver(err error) (ErrorClass, bool)
// IsDuplicateKey 报告 err 是否是该驱动的唯一冲突错误(TryClaim dup 检测用,D3)。
IsDuplicateKey(err error) bool
// ErrorCode 提取驱动原生错误码字符串(如 MySQL "1213"、PostgreSQL "23505"),
// 用于填充 error_code 列(有界、可聚合的稳定代码,§5)。返回 (code, true) 表示已提取,
// ("", false) 表示不认识的错误——调用方回落 Classify() 的 class 名作为 code。
ErrorCode(err error) (string, bool)
}
ErrorClassifier 是 driver classifier 的统一接口(§5 第 2 级)。 实现放 store/mysql(识别 *mysql.MySQLError.Number)与 store/postgres(识别 *pgconn.PgError.Code)。 kernel 不 import 任何数据库驱动——这就是为什么 classifier 是注入而非硬编码。 dup 检测(TryClaim 的 Create 竞态)也复用此接口,避免字符串匹配(D3)。
type HeaderPair ¶
HeaderPair 是有序、允许重复 key 的 header(镜像 eventbus.MessageHeader,但不依赖该包)。
type Key ¶
Key 是 event_consumption 的全局身份(§3.1):
- EventID 恒为 Envelope.EventID = 生产端 outbox 行 id(M5);
- Handler 稳定 HandlerID;
- ItemKey 单事件恒为空串;批量 item 填稳定业务身份(禁止用数组下标,M11 v2.6)。
type Meta ¶
Meta 描述性元数据(§3.1)。AggregateType/AggregateID 用于 §6.2.1 跨 topic/跨服务聚合分组; CausalSeq 是事件自带的领域版本号(没有则 nil,跨 topic 排序退化为同 topic 内可靠,v2.7)。
type MetaProvider ¶
type MetaProvider interface{ Meta() Meta }
MetaProvider 是默认提取方式。无法修改既有事件 DTO 时,装饰器接受显式 MetaFunc(§3.1)。
type NoOpAlerter ¶
type NoOpAlerter struct{}
NoOpAlerter 零实现(测试用)。
func (NoOpAlerter) AlertAnomaly ¶
func (NoOpAlerter) AlertAnomaly(string, HandlerID, string)
func (NoOpAlerter) AlertPoison ¶
func (NoOpAlerter) AlertPoison(HandlerID, string, error)
func (NoOpAlerter) AlertRecordFailure ¶
func (NoOpAlerter) AlertRecordFailure(HandlerID, error)
type NoOpMetrics ¶
type NoOpMetrics struct{}
NoOpMetrics 是零实现,供未接入指标的服务/测试使用(不得作为生产默认)。
func (NoOpMetrics) IncAnomaly ¶
func (NoOpMetrics) IncAnomaly(string, HandlerID)
func (NoOpMetrics) IncDeadLetter ¶
func (NoOpMetrics) IncDeadLetter(HandlerID, ErrorClass)
func (NoOpMetrics) IncRecordFailure ¶
func (NoOpMetrics) IncRecordFailure(HandlerID)
func (NoOpMetrics) IncReplayBlocked ¶
func (NoOpMetrics) IncReplayBlocked(HandlerID)
func (NoOpMetrics) SetPending ¶
func (NoOpMetrics) SetPending(Status, HandlerID, int, int64)
type PermanentError ¶
type PermanentError struct{ Cause error }
PermanentError 包装一个「永不可恢复」的根因:消息结构损坏或触发了永不会变的业务规则。 两者终点相同(DEAD_LETTER),排查思路不同——以 error_message 区分(§5 v2.6)。
func (*PermanentError) Error ¶
func (e *PermanentError) Error() string
func (*PermanentError) Unwrap ¶
func (e *PermanentError) Unwrap() error
type ReplaySafety ¶
type ReplaySafety int
ReplaySafety 按 handler 声明(§6.1)。未实现 ReplayableHandler 的 handler 默认 ReplayUnsafe。
const ( // ReplayUnsafe 默认值:有进程外副作用。可重试失败直接进 DEAD_LETTER(§6.1)。 ReplayUnsafe ReplaySafety = iota // ReplayNeedsTxClaim 会写新行,需事务化占位。 ReplayNeedsTxClaim // ReplayIdempotent 纯 upsert / delete-by-id / set-state,可安全自动重放。 ReplayIdempotent )
type ReplayableHandler ¶
type ReplayableHandler interface {
Handle(ctx context.Context, envelopeBytes []byte, delivery DeliveryMeta) error
HandlerID() HandlerID
ReplaySafety() ReplaySafety
RequiresAggregateGate() bool
}
ReplayableHandler 是可靠消费装饰器对 handler 的能力要求(§6.1)。
**签名偏离 spec §6.1(本轮评审 C3)**:spec §6.1 把 ReplayableHandler 定义为嵌入 `eventbus.EnvelopeDeliveryHandler`(收 `*Envelope` + `RawMeta`)。本计划改为 `Handle(ctx, []byte, DeliveryMeta)` ——raw envelope bytes + kernel 侧 DeliveryMeta 投影。这是刻意的 J2 决策(kernel 根包不得 import eventbus/sarama)。PR-3 的可靠消费 decorator 负责把 `env.ToBytes()` 的 bytes 解码回 `*Envelope` 再调 handler;spec §6.1 的 `EnvelopeDeliveryHandler` 嵌入由 decorator 层实现,不在 kernel 接口体现。
type RetryableError ¶
type RetryableError struct{ Cause error }
RetryableError 包装一个「可重试」的根因(瞬态 DB 超时/死锁/锁等待等)。
func (*RetryableError) Error ¶
func (e *RetryableError) Error() string
func (*RetryableError) Unwrap ¶
func (e *RetryableError) Unwrap() error
type TerminalOutcome ¶
type TerminalOutcome struct{ DeadLetter bool }
TerminalOutcome 是 MarkFailed 按 §6.1 矩阵推导的终点。
func OutcomeFor ¶
func OutcomeFor(class ErrorClass, safety ReplaySafety) TerminalOutcome
OutcomeFor 实现 §6.1 的 ErrorClass × ReplaySafety 矩阵(纯函数,双方言 store 共用)。
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
adapters
|
|
|
eventbus
Package eventbusdlq is the core DLQ adapter bridging the jxt-core partition- pipeline DLQSender contract to the reliable store (spec §7 "handler claim 前 / transport 失败" path).
|
Package eventbusdlq is the core DLQ adapter bridging the jxt-core partition- pipeline DLQSender contract to the reliable store (spec §7 "handler claim 前 / transport 失败" path). |
|
Package gate 提供 aggregate-gate 的获取/释放机制(§6.2.1)。
|
Package gate 提供 aggregate-gate 的获取/释放机制(§6.2.1)。 |
|
Package lease 周期**观测**过期 PROCESSING 租约(§3.2)。
|
Package lease 周期**观测**过期 PROCESSING 租约(§3.2)。 |
|
Package opsvc 是 reliable 内核的 §10 ops 服务层(PR-2 Task B2)。
|
Package opsvc 是 reliable 内核的 §10 ops 服务层(PR-2 Task B2)。 |
|
Package replay 实现 eligible-head 重放调度器(§6.2/§6.2.1)。
|
Package replay 实现 eligible-head 重放调度器(§6.2/§6.2.1)。 |
|
Package store 定义 reliable 持久化的方言中立抽象:Row 领域 model 与 Store 接口。
|
Package store 定义 reliable 持久化的方言中立抽象:Row 领域 model 与 Store 接口。 |
|
gormshared
Package gormshared 是 reliable Store 的共享 GORM 实现(D17)。
|
Package gormshared 是 reliable Store 的共享 GORM 实现(D17)。 |
|
mysql
Package mysql 是 reliable Store 的 MySQL 薄方言包(D17)。
|
Package mysql 是 reliable Store 的 MySQL 薄方言包(D17)。 |
|
postgres
Package postgres 是 reliable Store 的 PostgreSQL 薄方言包(D17)。
|
Package postgres 是 reliable Store 的 PostgreSQL 薄方言包(D17)。 |
|
repotest
Package repotest 是 reliable Store 的双方言共享 conformance suite(项目首创,准入 ⑭)。
|
Package repotest 是 reliable Store 的双方言共享 conformance suite(项目首创,准入 ⑭)。 |