feat: add bounded workflow decisions and durable result inbox

This commit is contained in:
2026-09-18 01:11:42 +08:00
parent 520e98d9d1
commit 0f7ac5203b
20 changed files with 1909 additions and 9 deletions

164
weave/engine/program.go Normal file
View File

@@ -0,0 +1,164 @@
package engine
import (
"encoding/json"
"errors"
"fmt"
"strings"
"unicode/utf8"
"git.yuntuo.hk/k2safe/orbit/internal/jsonwire"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/pulse/service"
"git.yuntuo.hk/k2safe/orbit/weave/definition"
weave "git.yuntuo.hk/k2safe/orbit/weave/model"
)
// TaskOwnerKind 标识由Weave激活拥有的Pulse任务;TASK及POLL轮次都使用激活ID作为owner_id。
const TaskOwnerKind = "workflow_activation"
// Program 持有独立拷贝的已验证规则,可并发读取;不暴露可变的内部映射。
type Program struct {
spec definition.Specification
nodes map[string]definition.Step
}
func Compile(spec definition.Specification) (*Program, error) {
if _, err := definition.Check(spec); err != nil {
return nil, err
}
raw, err := jsonwire.Marshal(spec)
if err != nil {
return nil, err
}
p := &Program{nodes: map[string]definition.Step{}}
if err := json.Unmarshal(raw, &p.spec); err != nil {
return nil, err
}
var visit func(definition.Block)
visit = func(b definition.Block) {
for _, s := range b.Steps {
p.nodes[s.Key] = s
if s.Branch != nil {
for _, c := range s.Branch.Cases {
visit(c.Body)
}
visit(*s.Branch.Default)
}
if s.Parallel != nil {
for _, branch := range s.Parallel.Branches {
visit(branch.Body)
}
}
}
}
visit(p.spec.Body)
return p, nil
}
// Step 返回定义副本,调用方修改不会改变已编译版本。
func (p *Program) Step(key string) (definition.Step, bool) {
s, ok := p.nodes[key]
if !ok {
return definition.Step{}, false
}
raw, err := jsonwire.Marshal(s)
if err != nil {
return definition.Step{}, false
}
var out definition.Step
if json.Unmarshal(raw, &out) != nil {
return out, false
}
return out, true
}
// State 是存储适配层核验身份/激活/版本后提供的当前步骤快照,不是直接信任的外部请求。
type State struct {
Status weave.StepState `json:"status"` // Status 逻辑步骤当前状态,BLOCKED可无激活。
Input json.RawMessage `json:"input,omitempty"` // Input 激活时冻结的参数或组合决策,重复推进不得重算。
Output json.RawMessage `json:"output,omitempty"` // Output 仅SUCCEEDED提供,显式null和未提供不同。
}
// Snapshot 必须来自同一实例事务内的持久化视图;节点不以内存缓存授予执行权。
type Snapshot struct {
WorkflowID core.ID // WorkflowID 固定实例标识,加入workflow引用上下文。
BusinessKey string // BusinessKey 业务追踪键,不代替幂等身份。
Input json.RawMessage // Input 启动时冻结且已保存的输入。
Steps map[string]State // Steps 当前激活投影;缺少的静态步骤视为BLOCKED。
AllowNew bool // AllowNew 已核验暂停、取消、截止时间和门控后是否允许激活新步骤。
}
// WaitCommand 只描述需要持久化的等待;存储层用数据库激活时间固定绝对截止。
type WaitCommand struct {
Kind string // Kind SIGNAL、TIMER或POLL最终回执。
Name string // Name 业务信号名称;TIMER为空。
PayloadVersion string // PayloadVersion 信号契约版本;TIMER为空。
Correlation string // Correlation 激活时冻结的业务操作,非定时等待必须非空。
TimeoutMS uint64 // TimeoutMS 相对激活时间的等待上界,还受实例绝对截止限制。
}
// Change 是一个必须与激活、任务/等待、检查点和审计同事务落地的决策。
type Change struct {
Key string // Key 对应静态步骤身份。
Operation string // Operation ACTIVATE、COMPLETE或SKIP。
Kind string // Kind 原定义节点类型,存储层仍需核对。
State State // State 变更后的步骤投影。
Task *service.SubmitInput // Task 已解析和校验的TASK/POLL输入,不包含实际任务ID或执行授权。
Wait *WaitCommand // Wait 需要持久化的等待信息;TIMER不占业务Worker。
}
// Decision 不含数据库副作用;只有存储事务提交后Changes才可用于后续工作。
type Decision struct {
Changes []Change // Changes 按依赖顺序的有界决策列表。
Complete bool // Complete 根块所有已选择步骤完成;不能替代存储层的在途结清校验。
Output json.RawMessage // Output Complete时的根块输出,已按契约验证。
More bool // More 因本轮步数预算停止,必须保存持久化待续记录。
}
// Problem 是可审计的稳定分类,不携带任意业务参数。
type Problem struct {
Key string
Code string
}
func (e *Problem) Error() string { return fmt.Sprintf("workflow step %q: %s", e.Key, e.Code) }
func problem(key, code string) error { return &Problem{Key: key, Code: code} }
// prepareTask 把冻结定义映射为Pulse受理参数;实际截止及额度由存储事务继续限制。
func prepareTask(t definition.Task, env values) (service.SubmitInput, error) {
raw, err := resolve(t.Input, t.InputType, env)
if err != nil {
return service.SubmitInput{}, err
}
in := service.SubmitInput{QueueID: t.QueueID, Handler: t.Handler, HandlerVersion: t.HandlerVersion, PayloadVersion: t.PayloadVersion, Payload: raw, TaskTimeoutMS: t.TimeoutMS, Retry: t.Retry, RecoverySafety: t.RecoverySafety, Priority: t.Priority}
for _, key := range []struct {
value *definition.Value
target *string
}{{t.BusinessKey, &in.BusinessKey}, {t.ConcurrencyKey, &in.ConcurrencyKey}} {
if key.value != nil {
raw, err := resolve(*key.value, definition.Type{Kind: "STRING"}, env)
if err != nil {
return in, err
}
if err := json.Unmarshal(raw, key.target); err != nil {
return in, err
}
}
}
return service.NormalizeSubmit(in)
}
func prepareSignal(s definition.Signal, kind string, env values) (*WaitCommand, error) {
raw, err := resolve(s.Correlation, definition.Type{Kind: "STRING"}, env)
if err != nil {
return nil, err
}
var correlation string
if json.Unmarshal(raw, &correlation) != nil || correlation == "" || len(correlation) > 512 {
return nil, errors.New("invalid runtime signal correlation")
}
return &WaitCommand{Kind: kind, Name: s.Name, PayloadVersion: s.PayloadVersion, Correlation: correlation, TimeoutMS: s.TimeoutMS}, nil
}
func validBusinessKey(s string) bool {
return utf8.ValidString(s) && utf8.RuneCountInString(s) <= 191 && !strings.ContainsRune(s, 0)
}