package mysql import ( "context" "database/sql" "encoding/json" "errors" "fmt" "time" "git.yuntuo.hk/k2safe/orbit/internal/fault" core "git.yuntuo.hk/k2safe/orbit/model" "git.yuntuo.hk/k2safe/orbit/platform/auth" pulse "git.yuntuo.hk/k2safe/orbit/pulse/model" "git.yuntuo.hk/k2safe/orbit/pulse/service" ) // CancelTask 将取消意图与开始授权串行化;已开始的尝试仍须确认结果或到期核查。 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 { view, err := readTask(ctx, tx, token.NamespaceID, id, false) if err != nil { return err } if !p.AllowsQueue(view.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 } // 应用额度 → 队列 → 任务 → 尝试 → 许可;取消不获取 Worker 会话锁。 if _, _, err := lockQueue(ctx, tx, token.NamespaceID, view.QueueID); err != nil { return err } task, err := readExecutionTask(ctx, tx, token.NamespaceID, id) if err != nil { return err } view = task.view if view.Revision != in.ExpectedRevision || view.State.Terminal() || view.CancelRequestedAt != nil { 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 } var attempt *pulse.Attempt if view.State == pulse.TaskLeased || view.State == pulse.TaskRunning { if task.current == nil { return errors.New("active task lacks attempt") } a, err := readAttempt(ctx, tx, token.NamespaceID, *task.current) if err != nil { return err } if a.TaskID != id || a.Epoch != task.epoch || string(a.State) != string(view.State) { return errors.New("task attempt identity or state mismatch") } attempt = &a } now, err := dbNow(ctx, tx) if err != nil { return err } next := view.State lease := task.lease var completed *time.Time if view.State == pulse.TaskWaiting || view.State == pulse.TaskLeased { next = pulse.TaskCancelled completed = &now lease = nil if view.EffectUncertain { next, completed = pulse.TaskNeedsReview, nil } if attempt != nil { if attempt.AuthorizedAt != nil { return errors.New("leased attempt already authorized") } result, err := tx.ExecContext(ctx, `UPDATE pulse_attempts SET state='CANCELLED',ended_at=?,error_code='CANCELLED_BEFORE_START',revision=revision+1,updated_at=? WHERE namespace_id=? AND id=? AND revision=?`, now, now, token.NamespaceID, attempt.ID, attempt.Revision) if err := exactlyOne(result, err); err != nil { return err } if err := releasePermits(ctx, tx, token.NamespaceID, attempt.ID, now); err != nil { return err } } } result, err := tx.ExecContext(ctx, `UPDATE pulse_tasks SET state=?,cancel_requested_at=?,lease_until=?,completed_at=?,updated_at=?,revision=revision+1 WHERE namespace_id=? AND id=? AND revision=?`, next, now, lease, completed, now, token.NamespaceID, id, in.ExpectedRevision) if err := exactlyOne(result, err); err != nil { return err } if next.Terminal() { if err := releaseQueueTask(ctx, tx, token.NamespaceID, view.QueueID); err != nil { return err } } out.Task, err = readTask(ctx, tx, token.NamespaceID, id, false) if err != nil { return err } event := resultEvent(next) if next == view.State { event = "task.cancel_requested" } if err := emitEvent(ctx, tx, token.NamespaceID, id, out.Task.Revision, "task", event, map[string]any{"task_id": id, "state": next, "reason": "CANCEL_REQUESTED", "effect_uncertain": view.EffectUncertain}); err != nil { return err } if err := appendAudit(ctx, tx, token, identity, id, "task", "task.cancel", in.Reason, map[string]any{"state": view.State, "revision": fmt.Sprint(view.Revision)}, map[string]any{"state": next, "revision": fmt.Sprint(out.Task.Revision), "cancel_requested_at": now}); err != nil { return err } return saveIdentity(ctx, tx, token.NamespaceID, identity, out) }) return out, err }