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

View File

@@ -0,0 +1,80 @@
package mysql
import (
"context"
"database/sql"
"errors"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/weave/engine"
)
type workflowTaskLink struct {
Owner sql.NullString
Activation *core.ID
Gate *core.ID
Linked bool
}
// appendWorkflowResult 在原Pulse结果事务内创建内部交付,必须先于外部结果流行锁。
// 这里只核验稳定归属并写入队列,不锁实例/激活、不推进流程、不锁对外订阅。
func appendWorkflowResult(ctx context.Context, tx *sql.Tx, ns, eventID, taskID core.ID, revision uint64, snapshot []byte, link workflowTaskLink) error {
owner, ownerID, gate := link.Owner, link.Activation, link.Gate
if !owner.Valid || owner.String != engine.TaskOwnerKind {
if owner.Valid && owner.String != "schedule" {
return errors.New("unrecognized task owner")
}
// 无外键情况下,丢失owner不能把已有流程关联静默降级成普通任务。
if link.Linked {
return errors.New("workflow result ownership missing")
}
return nil
}
if ownerID == nil || gate == nil {
return errors.New("workflow result owner or gate missing")
}
var workflow core.ID
var kind string
var activationTask *core.ID
// 即使旧激活不再是current,也要保存内部事件,消费方再核对当前激活并留痕。
err := tx.QueryRowContext(ctx, `SELECT a.workflow_id,s.kind,a.task_id
FROM weave_activations a
JOIN weave_steps s ON s.namespace_id=a.namespace_id AND s.id=a.step_id AND s.workflow_id=a.workflow_id
JOIN weave_instances i ON i.namespace_id=a.namespace_id AND i.id=a.workflow_id AND i.gate_id=?
JOIN pulse_execution_gates g ON g.namespace_id=i.namespace_id AND g.id=i.gate_id
JOIN weave_definition_versions v ON v.namespace_id=i.namespace_id AND v.id=i.definition_version_id AND v.definition_id=i.definition_id
JOIN weave_definitions d ON d.namespace_id=i.namespace_id AND d.id=i.definition_id
WHERE a.namespace_id=? AND a.id=?`, gate, ns, ownerID).Scan(&workflow, &kind, &activationTask)
if errors.Is(err, sql.ErrNoRows) {
return errors.New("workflow result associations invalid")
}
if err != nil {
return err
}
switch kind {
case "TASK":
if activationTask == nil || *activationTask != taskID {
return errors.New("workflow activation task mismatch")
}
case "POLL":
if activationTask != nil {
return errors.New("poll activation cannot own a direct task")
}
var round uint32
err = tx.QueryRowContext(ctx, `SELECT r.round FROM weave_poll_rounds r JOIN weave_poll_states p ON p.namespace_id=r.namespace_id AND p.workflow_id=r.workflow_id AND p.activation_id=r.activation_id WHERE r.namespace_id=? AND r.workflow_id=? AND r.activation_id=? AND r.task_id=? AND r.round BETWEEN 1 AND p.used_rounds`, ns, workflow, ownerID, taskID).Scan(&round)
if errors.Is(err, sql.ErrNoRows) {
return errors.New("workflow poll task association invalid")
}
if err != nil {
return err
}
default:
return errors.New("workflow task owner kind invalid")
}
id, err := core.NewID()
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `INSERT INTO weave_inbox(namespace_id,id,workflow_id,event_id,available_at,source_task_id,source_activation_id,source_revision,snapshot) VALUES (?,?,?,?,UTC_TIMESTAMP(6),?,?,?,?)`, ns, id, workflow, eventID, taskID, ownerID, revision, string(snapshot))
return err
}