feat: persist workflow timers and bounded poll rounds

This commit is contained in:
2026-09-18 03:03:31 +08:00
parent f602a5ee05
commit e2baffbe37
21 changed files with 1201 additions and 28 deletions

View File

@@ -0,0 +1,114 @@
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或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" {
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)
}
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" {
return r.advancePoll(ctx, tx, g, row, c, activated, 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)
}