267 lines
9.0 KiB
Go
267 lines
9.0 KiB
Go
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
|
||
}
|