feat: persist workflow controls and settle stopped branches

This commit is contained in:
2026-09-18 04:32:02 +08:00
parent 7d5ebfcb09
commit 7eac8c365a
35 changed files with 1539 additions and 36 deletions

48
weave/control/service.go Normal file
View File

@@ -0,0 +1,48 @@
// Package control 定义流程暂停、继续与取消的版本化操作;解除暂停不会清除核查或停止意图。
package control
import (
"context"
"strings"
"unicode/utf8"
"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/pulse/service"
"git.yuntuo.hk/k2safe/orbit/weave/instance"
)
const Action = "workflows:control"
// Input 的稳定操作身份由方法参数传入;并发控制只使用control_revision,不与后台结果版本竞争。
type Input struct {
Operation string `json:"operation"` // Operation 为PAUSE、RESUME或CANCEL;终结实例不接受新控制操作。
ExpectedControlRevision uint64 `json:"expected_control_revision,string"` // ExpectedControlRevision 调用者观察到的控制版本,必须大于0。
Reason string `json:"reason"` // Reason 必填操作原因,最多1024个Unicode字符,进入不可变审计。
}
type Result struct {
Instance instance.View `json:"instance"` // Instance 首次操作提交后的冻结回执;当前结果用实例Get读取。
Deduplicated bool `json:"deduplicated"` // Deduplicated 原操作恢复,不重复修改控制版本或计时。
}
type Store interface {
Apply(context.Context, auth.Token, core.ID, string, Input) (Result, error)
}
type Service struct{ store Store }
func New(store Store) *Service { return &Service{store: store} }
func (s *Service) Apply(ctx context.Context, t auth.Token, id core.ID, key string, in Input) (Result, error) {
if err := service.ValidateKey(key); err != nil {
return Result{}, err
}
if id.Validate() != nil || in.ExpectedControlRevision == 0 {
return Result{}, fault.Invalid("workflow id and positive control revision required")
}
if in.Operation != "PAUSE" && in.Operation != "RESUME" && in.Operation != "CANCEL" {
return Result{}, fault.Invalid("workflow operation must be PAUSE, RESUME or CANCEL")
}
if !utf8.ValidString(in.Reason) || strings.TrimSpace(in.Reason) == "" || utf8.RuneCountInString(in.Reason) > 1024 || strings.ContainsRune(in.Reason, 0) {
return Result{}, fault.Invalid("workflow control reason required, maximum 1024 characters")
}
return s.store.Apply(ctx, t, id, key, in)
}

View File

@@ -0,0 +1,43 @@
package control
import (
"context"
"strings"
"testing"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
)
type captureStore struct{ calls int }
func (s *captureStore) Apply(context.Context, auth.Token, core.ID, string, Input) (Result, error) {
s.calls++
return Result{}, nil
}
func TestControlRejectsUnsafeBoundary(t *testing.T) {
store := &captureStore{}
api := New(store)
id := core.ID("11111111111111111111111111111111")
base := Input{Operation: "PAUSE", ExpectedControlRevision: 1, Reason: "人工暂停"}
for _, operation := range []string{"PAUSE", "RESUME", "CANCEL"} {
in := base
in.Operation = operation
if _, err := api.Apply(context.Background(), auth.Token{}, id, "original", in); err != nil {
t.Fatal(err)
}
}
for _, change := range []func(*Input){func(in *Input) { in.ExpectedControlRevision = 0 }, func(in *Input) { in.Operation = "RETRY" }, func(in *Input) { in.Reason = " " }, func(in *Input) { in.Reason = strings.Repeat("中", 1025) }, func(in *Input) { in.Reason = "reason\x00hidden" }, func(in *Input) { in.Reason = string([]byte{0xff}) }} {
in := base
change(&in)
before := store.calls
if _, err := api.Apply(context.Background(), auth.Token{}, id, "original", in); err == nil || store.calls != before {
t.Fatal("invalid input reached store")
}
}
for _, key := range []string{"", strings.Repeat("x", 129)} {
if _, err := api.Apply(context.Background(), auth.Token{}, id, key, base); err == nil {
t.Fatal("invalid operation identity reached store")
}
}
}