feat: persist workflow controls and settle stopped branches
This commit is contained in:
136
internal/storage/mysql/workflow_controls.go
Normal file
136
internal/storage/mysql/workflow_controls.go
Normal file
@@ -0,0 +1,136 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user