Files
orbit/internal/storage/mysql/workflow_signal_source.go

183 lines
9.0 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 mysql
import (
"context"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"time"
"unicode/utf8"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/weave/definition"
"git.yuntuo.hk/k2safe/orbit/weave/signal"
)
type workflowSignalWait struct {
id core.ID
sequence uint32
state, correlation string
deadline, activated time.Time
winner *core.ID
contract *definition.Signal
}
// readWorkflowSignalWait 读取已经冻结的等待身份,不能把历史NULL元数据解释成有效信号契约。
func readWorkflowSignalWait(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep) (workflowSignalWait, error) {
var w workflowSignalWait
node, ok := g.program.Step(row.view.Key)
w.contract = signalContract(node)
if !ok || w.contract == nil || row.view.ActivationID == nil {
return w, errors.New("signal step definition invalid")
}
var count int
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM weave_waits WHERE namespace_id=? AND workflow_id=? AND activation_id=?`, g.view.NamespaceID, g.view.ID, row.view.ActivationID).Scan(&count); err != nil {
return w, err
}
if count != 1 {
return w, errors.New("signal wait set invalid")
}
var name, version *string
var kind string
err := tx.QueryRowContext(ctx, `SELECT w.id,w.kind,w.state,w.signal_name,w.payload_version,w.correlation_key,w.deadline_at,w.winner_signal_id,a.sequence,a.created_at FROM weave_waits w JOIN weave_activations a ON a.namespace_id=w.namespace_id AND a.workflow_id=w.workflow_id AND a.id=w.activation_id AND a.step_id=? WHERE w.namespace_id=? AND w.workflow_id=? AND w.activation_id=? FOR UPDATE`, row.view.ID, g.view.NamespaceID, g.view.ID, row.view.ActivationID).Scan(&w.id, &kind, &w.state, &name, &version, &w.correlation, &w.deadline, &w.winner, &w.sequence, &w.activated)
if err != nil {
return w, err
}
base, err := workflowActivationOrigin(ctx, tx, g, row)
if err != nil {
return w, err
}
w.activated = base.started
if base.sequence > 1 {
original := *row
original.view.ActivationID = &base.id
original.input = base.input
initial, err := readWorkflowSignalWait(ctx, tx, g, &original)
if err != nil {
return w, err
}
if initial.correlation != w.correlation {
return w, workflowSourceError("SIGNAL_RECOVERY_CORRELATION_INVALID")
}
}
if name == nil || version == nil {
return w, workflowSourceError("SIGNAL_WAIT_UNVERIFIED")
}
if w.sequence == 0 || kind != string(row.view.Kind) || *name != w.contract.Name || *version != w.contract.PayloadVersion || !utf8.ValidString(w.correlation) || len(w.correlation) == 0 || len(w.correlation) > 512 || !w.deadline.Equal(minimumTime(g.view.DeadlineAt, w.activated.Add(time.Duration(w.contract.TimeoutMS)*time.Millisecond))) {
return w, workflowSourceError("SIGNAL_WAIT_INVALID")
}
if node.Signal != nil {
var input struct {
Name string `json:"name"`
Version string `json:"payload_version"`
Correlation string `json:"correlation"`
}
if json.Unmarshal(row.input, &input) != nil || input.Name != *name || input.Version != *version || input.Correlation != w.correlation {
return w, workflowSourceError("SIGNAL_ACTIVATION_INPUT_INVALID")
}
}
return w, nil
}
// verifyWorkflowSignal 核对不可改写受理审计及请求摘要,防止可变状态读取替换原受理参数或时间。
func verifyWorkflowSignal(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep, w workflowSignalWait, v signal.View, now time.Time) error {
if v.NamespaceID != g.view.NamespaceID || v.WorkflowID != g.view.ID || v.TargetStepKey != row.view.Key || v.TargetActivationSequence != w.sequence || v.CorrelationKey != w.correlation || v.Name != w.contract.Name || v.PayloadVersion != w.contract.PayloadVersion || v.AcceptedAt.After(now) || !v.AcceptedAt.Before(w.deadline) || !v.ExpiresAt.Equal(g.view.DeadlineAt) || definition.ValidateData(w.contract.PayloadType, v.Payload) != nil {
return workflowSourceError("SIGNAL_IDENTITY_INVALID")
}
var digest core.Digest
if err := tx.QueryRowContext(ctx, `SELECT request_digest FROM weave_signals WHERE namespace_id=? AND workflow_id=? AND id=?`, g.view.NamespaceID, g.view.ID, v.ID).Scan(&digest); err != nil {
return err
}
var count int
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM orbit_audit_events WHERE namespace_id=? AND resource_kind='workflow_signal' AND resource_id=? AND action='workflow.signal'`, g.view.NamespaceID, v.ID).Scan(&count); err != nil {
return err
}
if count != 1 {
return workflowSourceError("SIGNAL_ACCEPTANCE_PROOF_MISSING")
}
var actor, reason string
var evidence sql.NullString
var raw []byte
if err := tx.QueryRowContext(ctx, "SELECT actor_id,reason,evidence_ref,`after` FROM orbit_audit_events WHERE namespace_id=? AND resource_kind='workflow_signal' AND resource_id=? AND action='workflow.signal'", g.view.NamespaceID, v.ID).Scan(&actor, &reason, &evidence, &raw); err != nil {
return err
}
var meta struct {
SignalID core.ID `json:"signal_id"`
WorkflowID core.ID `json:"workflow_id"`
Key string `json:"target_step_key"`
Sequence uint32 `json:"target_activation_sequence"`
Digest string `json:"request_digest"`
AcceptedAt time.Time `json:"accepted_at"`
ExpiresAt time.Time `json:"expires_at"`
}
if json.Unmarshal(raw, &meta) != nil || meta.SignalID != v.ID || meta.WorkflowID != g.view.ID || meta.Key != row.view.Key || meta.Sequence != w.sequence || meta.Digest != hex.EncodeToString(digest[:]) || !meta.AcceptedAt.Equal(v.AcceptedAt) || !meta.ExpiresAt.Equal(v.ExpiresAt) || actor != v.ActorID || evidence.String != v.EvidenceRef {
return workflowSourceError("SIGNAL_ACCEPTANCE_PROOF_INVALID")
}
expected, err := requestDigest(signal.Input{Name: v.Name, PayloadVersion: v.PayloadVersion, TargetStepKey: v.TargetStepKey, TargetActivationSequence: v.TargetActivationSequence, CorrelationKey: v.CorrelationKey, Payload: v.Payload, Reason: reason, EvidenceRef: v.EvidenceRef})
if err != nil {
return err
}
if expected != digest {
return workflowSourceError("SIGNAL_REQUEST_CHANGED")
}
return nil
}
// selectedWorkflowSignal 返回已胜出信号或最早按时受理的匹配信号;查询结果竞争由调用方在同一实例锁下裁决。
func selectedWorkflowSignal(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep, w workflowSignalWait, now time.Time) (*signal.View, error) {
var id core.ID
if w.winner != nil {
if w.state != "SATISFIED" {
return nil, workflowSourceError("SIGNAL_WINNER_WAIT_INVALID")
}
id = *w.winner
} else {
if w.state != "WAITING" {
return nil, nil
}
err := tx.QueryRowContext(ctx, `SELECT id FROM weave_signals WHERE namespace_id=? AND workflow_id=? AND target_step_key=? AND target_activation_sequence=? AND state='PENDING' AND type=? AND payload_version=? AND correlation_key=? AND accepted_at<? AND (? IS NULL OR accepted_at<=?) ORDER BY accepted_at,id LIMIT 1 FOR UPDATE`, g.view.NamespaceID, g.view.ID, row.view.Key, w.sequence, w.contract.Name, w.contract.PayloadVersion, []byte(w.correlation), w.deadline, g.view.StopRequestedAt, g.view.StopRequestedAt).Scan(&id)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
}
v, err := readWorkflowSignal(ctx, tx, g.view.NamespaceID, g.view.ID, id)
if err != nil {
return nil, err
}
if w.winner != nil && (v.State != "CONSUMED" || v.ConsumedWaitID == nil || *v.ConsumedWaitID != w.id) {
return nil, workflowSourceError("SIGNAL_WINNER_ASSOCIATION_INVALID")
}
if err := verifyWorkflowSignal(ctx, tx, g, row, w, v, now); err != nil {
return nil, err
}
return &v, nil
}
func claimWorkflowSignal(ctx context.Context, tx *sql.Tx, g *workflowGraph, w *workflowSignalWait, v *signal.View, now time.Time) error {
if w.winner != nil {
return nil
}
res, err := tx.ExecContext(ctx, `UPDATE weave_signals SET state='CONSUMED',disposition='WAIT_MATCHED',consumed_wait_id=?,revision=revision+1,updated_at=? WHERE namespace_id=? AND id=? AND workflow_id=? AND revision=? AND state='PENDING'`, w.id, now, g.view.NamespaceID, v.ID, g.view.ID, v.Revision)
if err := exactlyOne(res, err); err != nil {
return err
}
res, err = tx.ExecContext(ctx, `UPDATE weave_waits SET state='SATISFIED',winner_signal_id=?,resolved_at=?,revision=revision+1,updated_at=? WHERE namespace_id=? AND id=? AND workflow_id=? AND state='WAITING' AND winner_signal_id IS NULL`, v.ID, now, now, g.view.NamespaceID, w.id, g.view.ID)
if err := exactlyOne(res, err); err != nil {
return err
}
w.winner = &v.ID
w.state = "SATISFIED"
v.State = "CONSUMED"
v.Revision++
v.Disposition = "WAIT_MATCHED"
v.ConsumedWaitID = &w.id
return emitWorkflowSignalTransition(ctx, tx, g.view.NamespaceID, g.view.ID, v.ID, v.Revision, string(v.State), v.Disposition, v.ConsumedWaitID)
}
func rejectOtherWorkflowSignals(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep, sequence uint32, reason string, now time.Time) error {
return rejectPendingWorkflowSignals(ctx, tx, g, ` AND s.target_step_key=? AND s.target_activation_sequence=?`, []any{row.view.Key, sequence}, reason, now)
}