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

@@ -8,7 +8,6 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
@@ -318,80 +317,6 @@ func (s *TaskStore) ListTasks(ctx context.Context, token auth.Token, in service.
})
return out, err
}
func (s *TaskStore) CancelTask(ctx context.Context, token auth.Token, id core.ID, key string, in service.CancelInput) (service.CancelResult, error) {
var out service.CancelResult
err := s.transaction(ctx, token, "tasks:cancel", func(tx *sql.Tx, p auth.Permissions) error {
// 先以查询权限范围核对队列,再锁幂等记录,统一所有写操作的锁定顺序。
task, err := readTask(ctx, tx, token.NamespaceID, id, false)
if err != nil {
return err
}
if !p.AllowsQueue(task.QueueID) {
return fault.NotFound
}
identity, cached, err := reserveIdentity(ctx, tx, token.NamespaceID, "task.cancel", string(id), key, "task", id, in)
if err != nil {
return err
}
if cached != nil {
if err := json.Unmarshal(cached, &out); err != nil {
return err
}
out.Deduplicated = true
return nil
}
task, err = readTask(ctx, tx, token.NamespaceID, id, true)
if err != nil {
return err
}
if task.Revision != in.ExpectedRevision {
return fault.Conflict
}
if task.State != pulse.TaskWaiting {
return fault.Conflict
}
var owner sql.NullString
if err := tx.QueryRowContext(ctx, "SELECT owner_kind FROM pulse_tasks WHERE namespace_id=? AND id=?", token.NamespaceID, id).Scan(&owner); err != nil {
return err
}
if owner.Valid && owner.String != "schedule" {
return fault.Conflict
}
now, err := dbNow(ctx, tx)
if err != nil {
return err
}
next := pulse.TaskCancelled
var completed *time.Time = &now
if task.EffectUncertain {
next = pulse.TaskNeedsReview
completed = nil
}
result, err := tx.ExecContext(ctx, `UPDATE pulse_tasks SET state=?,cancel_requested_at=?,completed_at=?,updated_at=?,revision=revision+1 WHERE namespace_id=? AND id=? AND revision=? AND state='WAITING'`, next, now, completed, now, token.NamespaceID, id, in.ExpectedRevision)
if err != nil {
return err
}
n, err := result.RowsAffected()
if err != nil {
return err
}
if n != 1 {
return fault.Conflict
}
out.Task, err = readTask(ctx, tx, token.NamespaceID, id, false)
if err != nil {
return err
}
if err := emitEvent(ctx, tx, token.NamespaceID, id, out.Task.Revision, "task", resultEvent(out.Task.State), map[string]any{"task_id": id, "state": out.Task.State, "reason": "CANCEL_REQUESTED", "effect_uncertain": out.Task.EffectUncertain}); err != nil {
return err
}
if err := appendAudit(ctx, tx, token, identity, id, "task", "task.cancel", in.Reason, map[string]any{"state": task.State, "revision": fmt.Sprint(task.Revision)}, map[string]any{"state": out.Task.State, "revision": fmt.Sprint(out.Task.Revision)}); err != nil {
return err
}
return saveIdentity(ctx, tx, token.NamespaceID, identity, out)
})
return out, err
}
func (s *TaskStore) CreateQueue(ctx context.Context, token auth.Token, key string, in service.QueueInput) (service.QueueResult, error) {
var out service.QueueResult
err := s.transaction(ctx, token, "queues:manage", func(tx *sql.Tx, p auth.Permissions) error {