Files
orbit/pulse/retry/decision.go

125 lines
4.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package retry 以冻结策略和持久化预算决定下一状态,不执行任务或维护内存定时器。
package retry
import (
"crypto/sha256"
"encoding/binary"
"errors"
"time"
core "git.yuntuo.hk/k2safe/orbit/model"
pulse "git.yuntuo.hk/k2safe/orbit/pulse/model"
)
// Input 描述本次尝试结束时数据库已锁定的事实,不能从客户端覆盖预算和时间。
type Input struct {
Policy pulse.RetryPolicy // Policy 受理时冻结的有限策略。
Safety pulse.RecoverySafety // Safety 仅 IDEMPOTENT 允许未知效果自动恢复。
Consumed uint32 // Consumed 已领取次数,包含本次及未获批开始的尝试。
Outcome string // Outcome SUCCEEDED、FAILED、UNKNOWN、CANCELLED 或 NOT_STARTED。
Retryable bool // Retryable 仅对明确失败有效,Worker 声明可安全重试。
Uncertain bool // Uncertain 之前仍未核实的外部效果,失败或超时不能抹掉。
CancelRequested bool // CancelRequested 已保存取消意图时禁止后续尝试。
Now time.Time // Now 完成资源锁定后读取的数据库时间。
Deadline time.Time // Deadline 任务总截止时间。
StartDeadline *time.Time // StartDeadline 所有新尝试的最晚开始时间,可为空。
AttemptID core.ID // AttemptID 持久化尝试身份,用作可重现抖动的种子。
}
// Decision 必须与尝试结果、许可释放、事件及操作回执在同一事务提交。
type Decision struct {
State pulse.TaskState // State 下一任务状态,WAITING 不是业务终态。
Uncertain bool // Uncertain 跨尝试保留的不确定性;权威成功才自动清除。
NextAt *time.Time // NextAt 仅 WAITING 有值,写入 available_at。
Reason string // Reason 稳定决策分类,用于事件和审计。
DelayMS uint64 // DelayMS 实际等待毫秒数,非重试时为零。
}
func Decide(in Input) (Decision, error) {
out := Decision{State: pulse.TaskFailed, Uncertain: in.Uncertain, Reason: "NON_RETRYABLE_FAILURE"}
if err := in.Policy.Validate(); err != nil {
return out, err
}
if in.Consumed == 0 || in.Consumed > in.Policy.MaxAttempts || in.AttemptID.Validate() != nil || in.Now.IsZero() || in.Deadline.IsZero() {
return out, errors.New("invalid retry facts")
}
canRetry := false
switch in.Outcome {
case "SUCCEEDED":
return Decision{State: pulse.TaskSucceeded, Reason: "SUCCEEDED"}, nil
case "CANCELLED":
if !in.CancelRequested {
return out, errors.New("cancelled result requires persisted intent")
}
out.State, out.Reason = pulse.TaskCancelled, "CANCEL_CONFIRMED"
if in.Uncertain {
out.State, out.Reason = pulse.TaskNeedsReview, "RECONCILIATION_REQUIRED"
}
return out, nil
case "FAILED":
canRetry = in.Retryable
if canRetry {
out.Reason = "RETRYABLE_FAILURE"
}
case "UNKNOWN":
out.Uncertain = true
canRetry = in.Safety.CanRecoverAutomatically()
out.Reason = "RECONCILIATION_REQUIRED"
if canRetry {
out.Reason = "IDEMPOTENT_UNKNOWN"
}
case "NOT_STARTED":
canRetry = true
out.Reason = "UNSTARTED_LEASE_EXPIRED"
default:
return out, errors.New("invalid retry outcome")
}
if out.Uncertain {
out.State = pulse.TaskNeedsReview
if !in.Safety.CanRecoverAutomatically() {
canRetry = false
out.Reason = "RECONCILIATION_REQUIRED"
}
}
if in.CancelRequested {
out.Reason = "CANCEL_REQUESTED"
return out, nil
}
if !canRetry {
return out, nil
}
if in.Consumed >= in.Policy.MaxAttempts {
out.Reason = "ATTEMPT_LIMIT"
return out, nil
}
delay := DelayMS(in.Policy, in.Consumed, in.AttemptID)
next := in.Now.Add(time.Duration(delay) * time.Millisecond)
if !next.Before(in.Deadline) || (in.StartDeadline != nil && !next.Before(*in.StartDeadline)) {
out.Reason = "DEADLINE_EXCEEDED"
return out, nil
}
out.State = pulse.TaskWaiting
out.DelayMS = delay
out.NextAt = &next
return out, nil
}
// DelayMS 使用有上限的指数退避和 [ceil(base/2),base] 确定性抖动;策略须先 Validate。
// 相同尝试重复计算不改变结果,倍增先截断,避免次数或 Duration 溢出。
func DelayMS(policy pulse.RetryPolicy, consumed uint32, attempt core.ID) uint64 {
base := policy.InitialDelayMS
for n := uint32(1); n < consumed && base < policy.MaxDelayMS; n++ {
if base > policy.MaxDelayMS/2 {
base = policy.MaxDelayMS
break
}
base *= 2
}
if base > policy.MaxDelayMS {
base = policy.MaxDelayMS
}
lower := (base + 1) / 2
sum := sha256.Sum256([]byte("orbit.retry.v1:" + string(attempt)))
return lower + binary.BigEndian.Uint64(sum[:8])%(base-lower+1)
}