package mysql import ( "context" "database/sql" "encoding/json" "errors" "time" core "git.yuntuo.hk/k2safe/orbit/model" weave "git.yuntuo.hk/k2safe/orbit/weave/model" ) // workflowCheckpoint 是待续表中版本化的内部唤醒身份;不保存内存执行栈或新的业务预算。 type workflowCheckpoint struct { SchemaVersion uint32 `json:"schema_version"` // SchemaVersion 当前结构版本为1,未知版本禁止推进。 Kind string `json:"kind"` // Kind 为ROOT、TIMER、SIGNAL或POLL,决定实际权威记录的读取方式。 ActivationID *core.ID `json:"activation_id,omitempty"` // ActivationID 固定等待激活;ROOT不使用该字段。 } func saveWorkflowClock(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep, kind string, expected uint64, due time.Time) error { id, err := core.NewID() if err != nil { return err } if row.view.ActivationID == nil || kind != "TIMER" && kind != "POLL" && kind != "SIGNAL" { return errors.New("workflow clock identity invalid") } raw, err := json.Marshal(workflowCheckpoint{1, kind, row.view.ActivationID}) if err != nil { return err } _, err = tx.ExecContext(ctx, `INSERT INTO weave_pending_advances(namespace_id,id,workflow_id,step_id,expected_step_revision,checkpoint,available_at) VALUES (?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE expected_step_revision=VALUES(expected_step_revision),checkpoint=VALUES(checkpoint),available_at=VALUES(available_at),revision=revision+1,updated_at=UTC_TIMESTAMP(6)`, g.view.NamespaceID, id, g.view.ID, row.view.ID, expected, string(raw), due) return err } func deleteWorkflowClock(ctx context.Context, tx *sql.Tx, g *workflowGraph, row *workflowStep) error { _, err := tx.ExecContext(ctx, `DELETE FROM weave_pending_advances WHERE namespace_id=? AND workflow_id=? AND step_id=?`, g.view.NamespaceID, g.view.ID, row.view.ID) return err } func (r *WorkflowRuntime) advanceClock(ctx context.Context, tx *sql.Tx, g *workflowGraph, c workflowCandidate, step core.ID, expected uint64, checkpoint workflowCheckpoint, allow bool, now time.Time) (bool, error) { var row *workflowStep for _, candidate := range g.steps { if candidate.view.ID == step { row = candidate break } } if row == nil || checkpoint.ActivationID == nil || string(row.view.Kind) != checkpoint.Kind || expected > row.view.Revision { return false, errors.New("workflow clock checkpoint mismatch") } if row.view.ActivationID == nil || *row.view.ActivationID != *checkpoint.ActivationID || g.view.State.Terminal() { return false, deleteWorkflowClock(ctx, tx, g, row) } if row.view.State != weave.StepActive && row.view.State != weave.StepWaiting { return false, deleteWorkflowClock(ctx, tx, g, row) } if g.view.StopKind != nil { if err := saveWorkflowAdvance(ctx, tx, g, now); err != nil { return false, err } return false, deleteWorkflowClock(ctx, tx, g, row) } if checkpoint.Kind == "POLL" { node, _ := g.program.Step(row.view.Key) if node.Poll.FinalSignal != nil { handled, changed, reason, err := r.driveWorkflowSignal(ctx, tx, g, row, c, nil, allow, now) if err != nil { return false, err } if handled { if changed && reason != "" { return true, r.audit(ctx, tx, g, c.id, reason, now) } return changed, nil } } return r.advancePoll(ctx, tx, g, row, c, allow, now) } if checkpoint.Kind == "SIGNAL" { return r.advanceSignalClock(ctx, tx, g, row, c, allow, now) } if checkpoint.Kind != "TIMER" || row.view.State != weave.StepWaiting { return false, errors.New("workflow timer state invalid") } var wait core.ID var deadline time.Time var kind, state string var name, version sql.NullString var correlation []byte var count int if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM weave_waits WHERE namespace_id=? AND workflow_id=? AND activation_id=?`, c.ns, c.workflow, checkpoint.ActivationID).Scan(&count); err != nil { return false, err } if count != 1 { return false, errors.New("workflow timer wait set invalid") } if err := tx.QueryRowContext(ctx, `SELECT id,kind,state,deadline_at,signal_name,payload_version,correlation_key FROM weave_waits WHERE namespace_id=? AND workflow_id=? AND activation_id=? FOR UPDATE`, c.ns, c.workflow, checkpoint.ActivationID).Scan(&wait, &kind, &state, &deadline, &name, &version, &correlation); err != nil { return false, err } node, _ := g.program.Step(row.view.Key) origin, err := workflowActivationOrigin(ctx, tx, g, row) if err != nil { return false, err } intended := origin.started.Add(time.Duration(node.Timer.DelayMS) * time.Millisecond) if kind != "TIMER" || state != "WAITING" || name.Valid || version.Valid || len(correlation) != 0 || !deadline.Equal(minimumTime(intended, g.view.DeadlineAt)) { return false, errors.New("workflow timer wait invalid") } if now.Before(deadline) { return false, saveWorkflowClock(ctx, tx, g, row, "TIMER", row.view.Revision, deadline) } state, reason, stepState := "SATISFIED", "TIMER_ELAPSED", weave.StepSucceeded output := json.RawMessage(`null`) if intended.After(g.view.DeadlineAt) { state, reason, stepState, output = "EXPIRED", "TIMER_WORKFLOW_DEADLINE", weave.StepNeedsReview, nil } res, err := tx.ExecContext(ctx, `UPDATE weave_waits SET state=?,resolved_at=?,revision=revision+1,updated_at=? WHERE namespace_id=? AND id=? AND state='WAITING'`, state, now, now, c.ns, wait) if err := exactlyOne(res, err); err != nil { return false, err } if err := saveWorkflowResultStep(ctx, tx, g, row, stepState, output, now); err != nil { return false, err } if err := deleteWorkflowClock(ctx, tx, g, row); err != nil { return false, err } if stepState == weave.StepNeedsReview { if err := holdWorkflow(ctx, tx, g, weave.InstanceNeedsReview, reason, now); err != nil { return false, err } } else if err := r.progress(ctx, tx, g, allow, now); err != nil { return false, err } return true, r.audit(ctx, tx, g, c.id, reason, now) }