feat: support durable task cancellation without foreign keys

This commit is contained in:
2026-09-17 17:03:23 +08:00
parent 6805506450
commit 3ce7d2c104
48 changed files with 1642 additions and 164 deletions

View File

@@ -16,7 +16,7 @@ type Input struct {
Policy pulse.RetryPolicy // Policy 受理时冻结的有限策略。
Safety pulse.RecoverySafety // Safety 仅 IDEMPOTENT 允许未知效果自动恢复。
Consumed uint32 // Consumed 已领取次数,包含本次及未获批开始的尝试。
Outcome string // Outcome SUCCEEDED、FAILED、UNKNOWN 或 NOT_STARTED。
Outcome string // Outcome SUCCEEDED、FAILED、UNKNOWN、CANCELLED 或 NOT_STARTED。
Retryable bool // Retryable 仅对明确失败有效,Worker 声明可安全重试。
Uncertain bool // Uncertain 之前仍未核实的外部效果,失败或超时不能抹掉。
CancelRequested bool // CancelRequested 已保存取消意图时禁止后续尝试。
@@ -47,6 +47,15 @@ func Decide(in Input) (Decision, error) {
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 {

View File

@@ -117,7 +117,7 @@ func (s *Service) Finish(ctx context.Context, t auth.Token, b Binding, message c
if err := validCommand(message, in.Ref); err != nil {
return CommandResult{}, err
}
if in.Outcome != "SUCCEEDED" && in.Outcome != "FAILED" && in.Outcome != "UNKNOWN" {
if in.Outcome != "SUCCEEDED" && in.Outcome != "FAILED" && in.Outcome != "UNKNOWN" && in.Outcome != "CANCELLED" {
return CommandResult{}, fault.Invalid("invalid result outcome")
}
if in.Retryable && in.Outcome != "FAILED" {
@@ -140,3 +140,7 @@ func (s *Service) Resume(ctx context.Context, t auth.Token, b Binding, ref Attem
}
return s.store.Resume(ctx, t, b, ref)
}
func (s *Service) Cancellations(ctx context.Context, t auth.Token, b Binding) ([]Cancellation, error) {
return s.store.Cancellations(ctx, t, b)
}

View File

@@ -21,11 +21,12 @@ type Capability struct {
// ConnectInput 在每次连接时提交;同一进程恢复时保留实例和秘密。
type ConnectInput struct {
InstanceID core.ID `json:"instance_id"` // InstanceID 进程标识,重启换新值。
InstanceSecret string `json:"instance_secret"` // InstanceSecret 32 字节随机秘密,只保存摘要。
QueueID core.ID `json:"queue_id"` // QueueID 当前进程绑定的消费队列。
Capabilities []Capability `json:"capabilities"` // Capabilities 1–32 个精确版本组合,重连固定。
Capacity uint32 `json:"capacity"` // Capacity 总执行槽数量,1–256。
CancellationNotifications bool `json:"cancellation_notifications,omitempty"` // CancellationNotifications 显式接受 task.cancel 提示;事实仍从持久化租约核对。
InstanceID core.ID `json:"instance_id"` // InstanceID 进程标识,重启换新值。
InstanceSecret string `json:"instance_secret"` // InstanceSecret 32 字节随机秘密,只保存摘要。
QueueID core.ID `json:"queue_id"` // QueueID 当前进程绑定的消费队列。
Capabilities []Capability `json:"capabilities"` // Capabilities 1–32 个精确版本组合,重连固定。
Capacity uint32 `json:"capacity"` // Capacity 总执行槽数量,1–256。
}
// Binding 是接入层持有的认证结果,不作为公开请求字段接受或写入日志。
@@ -74,7 +75,7 @@ type AttemptRef struct {
// FinishInput 区分明确结果与未知效果,未知不能被解释为可以盲目重跑。
type FinishInput struct {
Ref AttemptRef `json:"ref"` // Ref 当前尝试授权证明。
Outcome string `json:"outcome"` // Outcome 为 SUCCEEDED、FAILED 或 UNKNOWN。
Outcome string `json:"outcome"` // Outcome 为 SUCCEEDED、FAILED、UNKNOWN 或确认本次停止且无遗留效果的 CANCELLED。
Result json.RawMessage `json:"result"` // Result 非 null JSON 结果或证据引用,最多 256 KiB。
ErrorCode string `json:"error_code,omitempty"` // ErrorCode FAILED/UNKNOWN 必填稳定分类,不包含原始异常正文。
Retryable bool `json:"retryable,omitempty"` // Retryable 仅 FAILED 可为真,声明该明确失败可安全重试;默认假。
@@ -93,10 +94,11 @@ type Receipt struct {
// LeaseStatus 每次响应重新计算;处理器仅在 Active 为真、State 为 RUNNING 且期限内推进。
// LEASED 的 Active 只代表预占仍有效,不能代替开始授权。
type LeaseStatus struct {
Active bool `json:"active"` // Active 当前会话仍持有未过期的有效尝试。
State pulse.AttemptState `json:"state"` // State 当前尝试状态。
LeaseUntil time.Time `json:"lease_until"` // LeaseUntil 当前租约,不因重放旧回执自动延长。
ServerTime time.Time `json:"server_time"` // ServerTime 完成锁定后重新读取的数据库时间。
CancelRequestedAt *time.Time `json:"cancel_requested_at,omitempty"` // CancelRequestedAt 已保存的取消时间;非空禁止继续业务处理,有效期内仍能确认结果。
Active bool `json:"active"` // Active 当前会话仍持有未过期的有效尝试。
State pulse.AttemptState `json:"state"` // State 当前尝试状态。
LeaseUntil time.Time `json:"lease_until"` // LeaseUntil 当前租约,不因重放旧回执自动延长。
ServerTime time.Time `json:"server_time"` // ServerTime 完成锁定后重新读取的数据库时间。
}
// CommandResult 将原回执与当前执行权分开,避免旧 start ACK 使过期尝试复活。
@@ -113,7 +115,17 @@ type RecoveryStats struct {
Failed int
NeedsReview int
}
// Cancellation 是当前会话拥有的运行尝试的持久化取消提示,不是结果确认。
type Cancellation struct {
TaskID core.ID `json:"task_id"` // TaskID 已授权任务。
AttemptID core.ID `json:"attempt_id"` // AttemptID 目标尝试,旧通知不得影响新尝试。
Epoch uint64 `json:"epoch,string"` // Epoch 任务执行代次。
RequestedAt time.Time `json:"requested_at"` // RequestedAt 首次取消意图的数据库时间。
}
type Store interface {
Cancellations(context.Context, auth.Token, Binding) ([]Cancellation, error)
Authorize(context.Context, auth.Token) error
Connect(context.Context, auth.Token, ConnectInput) (Binding, Welcome, error)
Capacity(context.Context, auth.Token, Binding, CapacityInput) (Welcome, error)