165 lines
6.4 KiB
Go
165 lines
6.4 KiB
Go
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)
|
||
}
|