Files
orbit/weave/engine/program.go

165 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}