137 lines
6.0 KiB
Go
137 lines
6.0 KiB
Go
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)
|
||
}
|
||
var activated time.Time
|
||
if err := tx.QueryRowContext(ctx, `SELECT created_at FROM weave_activations WHERE namespace_id=? AND id=? AND workflow_id=? AND step_id=?`, c.ns, checkpoint.ActivationID, c.workflow, step).Scan(&activated); err != nil {
|
||
return false, err
|
||
}
|
||
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, activated, 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)
|
||
intended := activated.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)
|
||
}
|