167 lines
8.5 KiB
Go
167 lines
8.5 KiB
Go
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
|
||
}
|
||
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<? 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).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 nil
|
||
}
|
||
func rejectOtherWorkflowSignals(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep, sequence uint32, reason string, now time.Time) error {
|
||
_, err := tx.ExecContext(ctx, `UPDATE weave_signals SET state='REJECTED',disposition=?,revision=revision+1,updated_at=? WHERE namespace_id=? AND workflow_id=? AND target_step_key=? AND target_activation_sequence=? AND state='PENDING'`, reason, now, g.view.NamespaceID, g.view.ID, row.view.Key, sequence)
|
||
return err
|
||
}
|