137 lines
5.1 KiB
Go
137 lines
5.1 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
|
|
"git.yuntuo.hk/k2safe/orbit/internal/fault"
|
|
core "git.yuntuo.hk/k2safe/orbit/model"
|
|
"git.yuntuo.hk/k2safe/orbit/platform/auth"
|
|
"git.yuntuo.hk/k2safe/orbit/weave/control"
|
|
weave "git.yuntuo.hk/k2safe/orbit/weave/model"
|
|
)
|
|
|
|
// WorkflowControlStore 控制决定和门控同事务,不在请求中等待外部执行停止。
|
|
type WorkflowControlStore struct{ *TaskStore }
|
|
|
|
func NewWorkflowControlStore(db *sql.DB) *WorkflowControlStore {
|
|
return &WorkflowControlStore{NewTaskStore(db)}
|
|
}
|
|
|
|
var _ control.Store = (*WorkflowControlStore)(nil)
|
|
|
|
func (s *WorkflowControlStore) Apply(ctx context.Context, t auth.Token, id core.ID, key string, in control.Input) (control.Result, error) {
|
|
var out control.Result
|
|
err := s.transaction(ctx, t, control.Action, func(tx *sql.Tx, p auth.Permissions) error {
|
|
if _, err := lockApplicationResources(ctx, tx, t.NamespaceID, false); err != nil {
|
|
return err
|
|
}
|
|
identity, cached, err := reserveIdentity(ctx, tx, t.NamespaceID, "workflow.control", string(id), key, "workflow", id, in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
g, _, err := lockWorkflowGraph(ctx, tx, t.NamespaceID, id, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, q := range g.version.Analysis.Queues {
|
|
if !p.AllowsQueue(q) {
|
|
return fault.NotFound
|
|
}
|
|
}
|
|
if cached != nil {
|
|
if err := json.Unmarshal(cached, &out); err != nil {
|
|
return err
|
|
}
|
|
if out.Instance.NamespaceID != t.NamespaceID || out.Instance.ID != id || out.Instance.DefinitionVersionID != g.view.DefinitionVersionID {
|
|
return errors.New("workflow control receipt mismatch")
|
|
}
|
|
out.Deduplicated = true
|
|
return nil
|
|
}
|
|
if g.view.State.Terminal() || g.view.ControlRevision != in.ExpectedControlRevision {
|
|
return fault.Conflict
|
|
}
|
|
before := g.view
|
|
now, err := dbNow(ctx, tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch in.Operation {
|
|
case "PAUSE":
|
|
if g.view.Paused {
|
|
return fault.Conflict
|
|
}
|
|
g.view.Paused = true
|
|
case "RESUME":
|
|
if !g.view.Paused {
|
|
return fault.Conflict
|
|
}
|
|
g.view.Paused = false
|
|
case "CANCEL":
|
|
if g.view.CancelRequestedAt != nil {
|
|
return fault.Conflict
|
|
}
|
|
g.view.CancelRequestedAt = &now
|
|
if err := recordWorkflowStop(ctx, tx, g, "CANCEL", now); err != nil {
|
|
return err
|
|
}
|
|
if g.view.State != weave.InstanceNeedsReview {
|
|
g.view.State = weave.InstanceCancelling
|
|
}
|
|
default:
|
|
return fault.Invalid("unsupported workflow control operation")
|
|
}
|
|
res, err := tx.ExecContext(ctx, `UPDATE weave_instances SET paused=?,state=?,cancel_requested_at=?,control_revision=control_revision+1,revision=revision+1,updated_at=? WHERE namespace_id=? AND id=? AND control_revision=? AND revision=?`, g.view.Paused, g.view.State, g.view.CancelRequestedAt, now, t.NamespaceID, id, in.ExpectedControlRevision, g.view.Revision)
|
|
if err := exactlyOne(res, err); err != nil {
|
|
return err
|
|
}
|
|
g.view.ControlRevision++
|
|
g.view.Revision++
|
|
gatePaused := g.view.Paused || g.view.StopKind != nil || g.view.State == weave.InstanceNeedsReview || g.view.State == weave.InstanceCancelling
|
|
if _, err := tx.ExecContext(ctx, `UPDATE pulse_execution_gates SET paused=?,revision=revision+1,updated_at=? WHERE namespace_id=? AND id=?`, gatePaused, now, t.NamespaceID, g.gate); err != nil {
|
|
return err
|
|
}
|
|
if in.Operation == "CANCEL" || in.Operation == "RESUME" {
|
|
if err := saveWorkflowAdvance(ctx, tx, g, now); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
meta := map[string]any{"operation": in.Operation, "paused": g.view.Paused, "state": g.view.State, "control_revision": g.view.ControlRevision, "stop_kind": g.view.StopKind, "stop_requested_at": g.view.StopRequestedAt, "cancel_requested_at": g.view.CancelRequestedAt}
|
|
if err := emitEvent(ctx, tx, t.NamespaceID, id, g.view.Revision, "workflow", "workflow.controlled", meta); err != nil {
|
|
return err
|
|
}
|
|
if err := appendAudit(ctx, tx, t, identity, id, "workflow", "workflow.control", in.Reason, map[string]any{"paused": before.Paused, "state": before.State, "control_revision": before.ControlRevision, "stop_kind": before.StopKind}, meta); err != nil {
|
|
return err
|
|
}
|
|
out.Instance, _, err = readWorkflowInstance(ctx, tx, t.NamespaceID, id, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return saveIdentity(ctx, tx, t.NamespaceID, identity, out)
|
|
})
|
|
return out, err
|
|
}
|
|
|
|
// recordWorkflowStop 冻结首次停止原因;后来的用户取消不能抹去已启动的失败结清。
|
|
// 调用方同时关闭门控、保存根检查点和审计,不能将此函数单独提交。
|
|
func recordWorkflowStop(ctx context.Context, tx *sql.Tx, g *workflowGraph, kind string, now time.Time) error {
|
|
if g.view.StopKind != nil {
|
|
return nil
|
|
}
|
|
if kind != "CANCEL" && kind != "FAILURE" {
|
|
return errors.New("invalid workflow stop kind")
|
|
}
|
|
res, err := tx.ExecContext(ctx, `UPDATE weave_instances SET stop_kind=?,stop_requested_at=?,revision=revision+1,updated_at=? WHERE namespace_id=? AND id=? AND revision=? AND stop_kind IS NULL`, kind, now, now, g.view.NamespaceID, g.view.ID, g.view.Revision)
|
|
if err := exactlyOne(res, err); err != nil {
|
|
return err
|
|
}
|
|
g.view.StopKind = &kind
|
|
g.view.StopRequestedAt = &now
|
|
g.view.Revision++
|
|
return nil
|
|
}
|