feat: trace task sources to fixed workflow activations and rounds

This commit is contained in:
2026-09-18 07:14:55 +08:00
parent b9e4191c6b
commit ae0d08584a
27 changed files with 593 additions and 47 deletions

View File

@@ -0,0 +1,99 @@
package mysql
import (
"context"
"database/sql"
"encoding/json"
"errors"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/pulse/service"
"git.yuntuo.hk/k2safe/orbit/weave/engine"
)
// readTaskWorkflowSource 只核验创建时固定的来源,不按当前激活替换历史,也不取得运行器锁。
// 这里只向已授权任务暴露有限引用;完整流程及定义继续使用各自权限。
func readTaskWorkflowSource(ctx context.Context, tx *sql.Tx, ns, task, queue, activation, gate core.ID, direct bool) (service.TaskWorkflowSource, error) {
var out service.TaskWorkflowSource
var activationTask *core.ID
var policyRaw []byte
var handler, handlerVersion, payloadVersion string
err := tx.QueryRowContext(ctx, "SELECT a.workflow_id,i.definition_id,i.definition_version_id,v.version,s.id,s.`key`,s.kind,a.id,a.sequence,a.task_id,i.policy_snapshot,t.retry_of,t.handler,t.handler_version,t.payload_version FROM weave_activations a JOIN weave_steps s ON s.namespace_id=a.namespace_id AND s.workflow_id=a.workflow_id AND s.id=a.step_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_definitions d ON d.namespace_id=i.namespace_id AND d.id=i.definition_id JOIN weave_definition_versions v ON v.namespace_id=i.namespace_id AND v.definition_id=i.definition_id AND v.id=i.definition_version_id JOIN pulse_tasks t ON t.namespace_id=a.namespace_id AND t.id=? AND t.owner_kind='workflow_activation' AND t.owner_id=a.id AND t.gate_id=i.gate_id AND t.queue_id=? JOIN pulse_queues q ON q.namespace_id=t.namespace_id AND q.id=t.queue_id WHERE a.namespace_id=? AND a.id=?", gate, task, queue, ns, activation).Scan(&out.WorkflowID, &out.DefinitionID, &out.DefinitionVersionID, &out.Version, &out.StepID, &out.StepKey, &out.StepKind, &out.ActivationID, &out.ActivationSequence, &activationTask, &policyRaw, &out.RetryOf, &handler, &handlerVersion, &payloadVersion)
if errors.Is(err, sql.ErrNoRows) {
return out, fault.Unavailable
}
if err != nil {
return out, err
}
version, err := readWorkflowVersion(ctx, tx, ns, out.DefinitionID, out.Version)
if err != nil {
return out, err
}
var policy workflowPolicy
if version.ID != out.DefinitionVersionID || json.Unmarshal(policyRaw, &policy) != nil || policy != (workflowPolicy{1, version.ID, version.ContentDigest, version.Specification.FailurePolicy, version.Analysis.MaxTaskAttempts}) {
return out, fault.Unavailable
}
program, err := engine.Compile(version.Specification)
if err != nil {
return out, err
}
node, ok := program.Step(out.StepKey)
if !ok || node.Kind != out.StepKind || out.ActivationSequence == 0 {
return out, fault.Unavailable
}
contract := node.Task
wantsParent := out.ActivationSequence > 1
switch out.StepKind {
case "TASK":
if !direct || activationTask == nil || *activationTask != task || contract == nil || out.ActivationSequence > contract.Retry.MaxAttempts {
return out, fault.Unavailable
}
case "POLL":
if direct || activationTask != nil || node.Poll == nil {
return out, fault.Unavailable
}
contract = &node.Poll.Task
var first, used, maximum uint32
var roundID core.ID
err = tx.QueryRowContext(ctx, "SELECT r.id,r.`round`,p.first_round,p.used_rounds,p.max_rounds 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=?", ns, out.WorkflowID, activation, task).Scan(&roundID, &out.PollRound, &first, &used, &maximum)
if errors.Is(err, sql.ErrNoRows) {
return out, fault.Unavailable
}
if err != nil {
return out, err
}
if first == 0 || first > out.PollRound || out.PollRound > used || used > maximum || maximum != node.Poll.MaxRounds || out.ActivationSequence == 1 && first != 1 || out.ActivationSequence > maximum*contract.Retry.MaxAttempts {
return out, fault.Unavailable
}
out.PollRoundID = &roundID
wantsParent = wantsParent && out.PollRound == first
default:
return out, fault.Unavailable
}
if contract.QueueID != queue || contract.Handler != handler || contract.HandlerVersion != handlerVersion || contract.PayloadVersion != payloadVersion || wantsParent != (out.RetryOf != nil) {
return out, fault.Unavailable
}
if wantsParent {
if *out.RetryOf == task {
return out, fault.Unavailable
}
// 只读直接父任务,使用步骤/代次唯一索引;不扫描或递归整个恢复历史。
q := `SELECT COUNT(*) FROM weave_activations a JOIN pulse_tasks t ON t.namespace_id=a.namespace_id AND t.owner_kind='workflow_activation' AND t.owner_id=a.id AND t.gate_id=? AND t.queue_id=? AND t.id=? WHERE a.namespace_id=? AND a.workflow_id=? AND a.step_id=? AND a.sequence=? AND t.effect_uncertain=FALSE`
args := []any{gate, queue, *out.RetryOf, ns, out.WorkflowID, out.StepID, out.ActivationSequence - 1}
if out.StepKind == "TASK" {
q += ` AND a.task_id=t.id AND t.state IN ('FAILED','CANCELLED')`
} else {
q += " AND a.task_id IS NULL AND t.state IN ('SUCCEEDED','FAILED','CANCELLED') AND EXISTS(SELECT 1 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=a.namespace_id AND r.workflow_id=a.workflow_id AND r.activation_id=a.id AND r.task_id=t.id AND r.`round`=? AND p.used_rounds=r.`round` AND p.first_round<=r.`round`)"
args = append(args, out.PollRound)
}
var n int
if err = tx.QueryRowContext(ctx, q, args...).Scan(&n); err != nil {
return out, err
}
if n != 1 {
return out, fault.Unavailable
}
}
return out, nil
}