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

267 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/json"
"errors"
"strings"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
"git.yuntuo.hk/k2safe/orbit/pulse/service"
"git.yuntuo.hk/k2safe/orbit/weave/instance"
)
const workflowEventColumns = `e.id,e.aggregate_kind,e.aggregate_id,e.aggregate_revision,e.type,e.occurred_at,IF(OCTET_LENGTH(CAST(e.payload AS CHAR))<=65536,e.payload,NULL)`
const workflowEventTables = ` FROM orbit_events e FORCE INDEX(ix_event_workflow_history)
LEFT JOIN weave_steps s ON e.aggregate_kind='workflow_step' AND s.namespace_id=e.namespace_id AND s.id=e.aggregate_id AND s.workflow_id=e.workflow_id
WHERE e.namespace_id=? AND e.workflow_id=? AND (e.aggregate_kind<>'workflow_step' OR s.id IS NULL OR s.key<>'$root')`
type workflowEventParent struct {
view instance.View
steps map[core.ID]instance.StepView
}
func readWorkflowEventParent(ctx context.Context, tx *sql.Tx, ns, id core.ID, p auth.Permissions) (workflowEventParent, error) {
var out workflowEventParent
view, gate, ver, err := readWorkflowForQuery(ctx, tx, ns, id, p)
if err != nil {
return out, err
}
steps, err := readWorkflowStepViews(ctx, tx, ns, id, gate, ver, "")
if err != nil {
return out, err
}
out.view = view
out.steps = map[core.ID]instance.StepView{}
for _, step := range steps {
out.steps[step.ID] = step
}
return out, nil
}
func scanWorkflowEvent(row scanner) (instance.EventDetail, error) {
var out instance.EventDetail
var raw []byte
err := row.Scan(&out.ID, &out.AggregateKind, &out.AggregateID, &out.AggregateRevision, &out.Type, &out.OccurredAt, &raw)
out.Payload = raw
return out, err
}
func eventIDField(fields map[string]json.RawMessage, name string) (core.ID, error) {
var id core.ID
if json.Unmarshal(fields[name], &id) != nil || id.Validate() != nil {
return "", errors.New("invalid workflow event reference")
}
return id, nil
}
func verifyWorkflowEvent(ctx context.Context, tx *sql.Tx, h workflowEventParent, e *instance.EventDetail) error {
if e.ID.Validate() != nil || e.AggregateID.Validate() != nil || e.AggregateRevision == 0 || len(e.Payload) == 0 {
return errors.New("workflow event metadata invalid or oversized")
}
var fields map[string]json.RawMessage
if json.Unmarshal(e.Payload, &fields) != nil || fields == nil || json.Unmarshal(fields["state"], &e.State) != nil {
return errors.New("workflow event state missing")
}
e.WorkflowID = h.view.ID
switch e.AggregateKind {
case "workflow":
if e.AggregateID != h.view.ID || e.AggregateRevision > h.view.Revision {
return errors.New("workflow event aggregate mismatch")
}
e.StateScope = "WORKFLOW"
switch e.Type {
case "workflow.started":
def, err := eventIDField(fields, "definition_id")
if err != nil || def != h.view.DefinitionID {
return errors.New("workflow event definition mismatch")
}
version, err := eventIDField(fields, "definition_version_id")
if err != nil || version != h.view.DefinitionVersionID {
return errors.New("workflow event fixed version mismatch")
}
case "workflow.advanced", "workflow.controlled", "workflow.recovered", "workflow.blocked", "workflow.stopped":
case "workflow.signal.accepted":
e.StateScope = "SIGNAL"
signal, err := eventIDField(fields, "signal_id")
if err != nil {
return err
}
if err = verifyEventSignal(ctx, tx, h, signal, 0); err != nil {
return err
}
owner, err := eventIDField(fields, "workflow_id")
if err != nil || owner != h.view.ID {
return errors.New("workflow signal event scope mismatch")
}
default:
return errors.New("unsupported workflow event type")
}
case "workflow_step":
step, ok := h.steps[e.AggregateID]
if !ok || e.AggregateRevision > step.Revision || e.Type != "workflow.step.changed" {
return errors.New("workflow step event aggregate mismatch")
}
owner, err := eventIDField(fields, "workflow_id")
if err != nil || owner != h.view.ID {
return errors.New("workflow step event scope mismatch")
}
e.StepKey = step.Key
e.StateScope = "STEP"
if raw, ok := fields["key"]; ok {
var key string
if json.Unmarshal(raw, &key) != nil || key != step.Key {
return errors.New("workflow event step key mismatch")
}
}
if raw, ok := fields["activation_id"]; ok && string(raw) != "null" {
activation, err := eventIDField(fields, "activation_id")
if err != nil {
return err
}
var count int
if err = tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM weave_activations WHERE namespace_id=? AND workflow_id=? AND step_id=? AND id=?`, h.view.NamespaceID, h.view.ID, step.ID, activation).Scan(&count); err != nil {
return err
}
if count != 1 {
return errors.New("workflow event historical activation mismatch")
}
}
case "workflow_signal":
e.StateScope = "SIGNAL"
if e.Type != "workflow.signal.changed" {
return errors.New("unsupported workflow signal event")
}
owner, err := eventIDField(fields, "workflow_id")
if err != nil || owner != h.view.ID {
return errors.New("workflow signal event scope mismatch")
}
signal, err := eventIDField(fields, "signal_id")
if err != nil || signal != e.AggregateID {
return errors.New("workflow event signal reference mismatch")
}
if err = verifyEventSignal(ctx, tx, h, signal, e.AggregateRevision); err != nil {
return err
}
default:
return errors.New("unsupported workflow event aggregate")
}
valid := false
switch e.StateScope {
case "WORKFLOW":
switch e.State {
case "RUNNING", "WAITING", "CANCELLING", "NEEDS_REVIEW", "SUCCEEDED", "FAILED", "CANCELLED":
valid = true
}
case "STEP":
switch e.State {
case "BLOCKED", "ACTIVE", "WAITING", "NEEDS_REVIEW", "SUCCEEDED", "FAILED", "CANCELLED", "SKIPPED":
valid = true
}
case "SIGNAL":
switch e.State {
case "PENDING", "CONSUMED", "EXPIRED", "REJECTED":
valid = true
}
}
if !valid {
return errors.New("workflow event recorded state invalid")
}
return nil
}
func verifyEventSignal(ctx context.Context, tx *sql.Tx, h workflowEventParent, id core.ID, revision uint64) error {
var actual uint64
err := tx.QueryRowContext(ctx, `SELECT revision FROM weave_signals WHERE namespace_id=? AND workflow_id=? AND id=?`, h.view.NamespaceID, h.view.ID, id).Scan(&actual)
if err != nil {
return err
}
if revision > actual {
return errors.New("workflow signal event version mismatch")
}
return nil
}
func (s *WorkflowInstanceStore) ListEvents(ctx context.Context, t auth.Token, id core.ID, in service.HistoryPageInput) (instance.EventPage, error) {
out := instance.EventPage{WorkflowID: id, Items: []instance.EventSummary{}}
err := s.transaction(ctx, t, instance.AuditAction, func(tx *sql.Tx, p auth.Permissions) error {
h, err := readWorkflowEventParent(ctx, tx, t.NamespaceID, id, p)
if err != nil {
return err
}
out.Revision = h.view.Revision
q := `SELECT ` + workflowEventColumns + workflowEventTables
args := []any{t.NamespaceID, id}
if in.Cursor != "" {
c, err := decodeHistoryCursorKind(in.Cursor, t.NamespaceID, id, "", "workflow-events")
if err != nil {
return err
}
q += ` AND (e.occurred_at<? OR (e.occurred_at=? AND e.id<?))`
args = append(args, *c.Created, *c.Created, c.ID)
}
q += ` ORDER BY e.occurred_at DESC,e.id DESC LIMIT ?`
args = append(args, in.Limit+1)
rows, err := tx.QueryContext(ctx, q, args...)
if err != nil {
return err
}
var entries []instance.EventDetail
for rows.Next() {
e, err := scanWorkflowEvent(rows)
if err != nil {
rows.Close()
return err
}
entries = append(entries, e)
}
err = rows.Err()
rows.Close()
if err != nil {
return err
}
for i := range entries {
if err = verifyWorkflowEvent(ctx, tx, h, &entries[i]); err != nil {
return err
}
}
more := len(entries) > in.Limit
if more {
entries = entries[:in.Limit]
}
for _, e := range entries {
out.Items = append(out.Items, e.EventSummary)
}
if more {
last := out.Items[len(out.Items)-1]
out.NextCursor, err = encodeHistoryCursor(historyCursor{Version: 1, Kind: "workflow-events", Namespace: t.NamespaceID, Task: id, ID: last.ID, Created: &last.OccurredAt})
}
return err
})
if err != nil {
return instance.EventPage{}, err
}
return out, nil
}
func (s *WorkflowInstanceStore) GetEvent(ctx context.Context, t auth.Token, id, event core.ID) (instance.EventDetail, error) {
var out instance.EventDetail
err := s.transaction(ctx, t, instance.AuditAction, func(tx *sql.Tx, p auth.Permissions) error {
h, err := readWorkflowEventParent(ctx, tx, t.NamespaceID, id, p)
if err != nil {
return err
}
// 明确事件ID后走主键;仍保留流程派生范围和实际归属核验。
q := `SELECT ` + workflowEventColumns + strings.Replace(workflowEventTables, "FORCE INDEX(ix_event_workflow_history)", "FORCE INDEX(PRIMARY)", 1) + ` AND e.id=?`
out, err = scanWorkflowEvent(tx.QueryRowContext(ctx, q, t.NamespaceID, id, event))
if errors.Is(err, sql.ErrNoRows) {
return fault.NotFound
}
if err != nil {
return err
}
return verifyWorkflowEvent(ctx, tx, h, &out)
})
if err != nil {
return instance.EventDetail{}, err
}
return out, nil
}