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

@@ -181,7 +181,7 @@ func (s *ExecutionStore) Claim(ctx context.Context, token auth.Token, b worker.B
if !state.expires.After(now) {
return worker.StaleSession
}
if task.view.State != pulse.TaskWaiting || task.view.AvailableAt.After(now) || !task.view.DeadlineAt.After(now) || task.view.AttemptCount >= task.maxAttempts || (task.view.StartDeadlineAt != nil && !task.view.StartDeadlineAt.After(now)) {
if task.view.State != pulse.TaskWaiting || task.view.CancelRequestedAt != nil || task.view.AvailableAt.After(now) || !task.view.DeadlineAt.After(now) || task.view.AttemptCount >= task.maxAttempts || (task.view.StartDeadlineAt != nil && !task.view.StartDeadlineAt.After(now)) {
continue
}
if task.epoch == ^uint64(0) {
@@ -327,7 +327,7 @@ func leaseStatus(rows executionRows, b worker.Binding, now time.Time) worker.Lea
active = active && deadline.After(now)
until = minimumTime(until, deadline)
}
return worker.LeaseStatus{Active: active, State: a.State, LeaseUntil: until, ServerTime: now}
return worker.LeaseStatus{CancelRequestedAt: t.view.CancelRequestedAt, Active: active, State: a.State, LeaseUntil: until, ServerTime: now}
}
func attemptIdentity(ref worker.AttemptRef) any {
return struct {
@@ -453,6 +453,9 @@ func (s *ExecutionStore) executionCommand(ctx context.Context, token auth.Token,
task, a := rows.task, rows.attempt
authorized := a.AuthorizedAt
if operation == "start" {
if task.view.CancelRequestedAt != nil {
return worker.LeaseLost
}
if a.State != pulse.AttemptLeased || task.view.State != pulse.TaskLeased {
return fault.Conflict
}
@@ -466,6 +469,15 @@ func (s *ExecutionStore) executionCommand(ctx context.Context, token auth.Token,
} else if a.State != pulse.AttemptRunning || authorized == nil {
return fault.Conflict
}
if task.view.CancelRequestedAt != nil {
// 取消后不延长执行窗口;当前权威结果仍可在原租约内提交。
receipt := worker.Receipt{Operation: operation, TaskState: task.view.State, AttemptState: a.State, TaskRevision: task.view.Revision, LeaseUntil: status.LeaseUntil, Disposition: "APPLIED"}
if err := saveReceipt(ctx, tx, token.NamespaceID, a.ID, message, operation, digest, receipt); err != nil {
return err
}
out = worker.CommandResult{Receipt: receipt, Lease: status}
return nil
}
until := minimumTime(now.Add(s.options.LeaseDuration), authorized.Add(time.Duration(task.attemptTimeout)*time.Millisecond), task.view.DeadlineAt, rows.session.expires)
result, err := tx.ExecContext(ctx, `UPDATE pulse_attempts SET state='RUNNING',authorized_at=?,lease_until=?,revision=revision+1,updated_at=? WHERE namespace_id=? AND id=? AND revision=?`, authorized, until, now, token.NamespaceID, a.ID, a.Revision)
if err := exactlyOne(result, err); err != nil {