feat: add bounded workflow decisions and durable result inbox
This commit is contained in:
80
internal/storage/mysql/workflow_events.go
Normal file
80
internal/storage/mysql/workflow_events.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user