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

137 lines
6.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"
"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, 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)
}