feat: expose workflow state events and persist signal transitions

This commit is contained in:
2026-09-18 09:27:45 +08:00
parent 082898e705
commit a4d9900e2c
34 changed files with 1499 additions and 27 deletions

View File

@@ -0,0 +1,97 @@
package mysql
import (
"context"
"database/sql"
"encoding/json"
"errors"
"strings"
"time"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/weave/signal"
)
type signalTransition struct {
WorkflowID core.ID `json:"workflow_id"` // WorkflowID 与信号归属同事务保存的流程身份。
SignalID core.ID `json:"signal_id"` // SignalID 被修改的信号身份,旧记录保留。
From string `json:"from"` // From 本次转换前的真实状态,不以当前状态推测过去。
State string `json:"state"` // State 本次转换提交的目标状态。
Disposition string `json:"disposition"` // Disposition 消费、过期或拒绝的稳定原因。
ConsumedWaitID *core.ID `json:"consumed_wait_id,omitempty"` // ConsumedWaitID 消费赢家等待,其他转换省略。
}
func emitWorkflowSignalTransition(ctx context.Context, tx *sql.Tx, ns, workflow, id core.ID, revision uint64, state, reason string, wait *core.ID) error {
return emitEvent(ctx, tx, ns, id, revision, "workflow_signal", "workflow.signal.changed", signalTransition{workflow, id, "PENDING", state, reason, wait})
}
// rejectPendingWorkflowSignals 的filter只由内部固定SQL构造。调用方已持有实例写锁,
// 一次读取最多1025条检测破坏上限的存储;更新和每条事件同事务,按32条批量追加事件。
func rejectPendingWorkflowSignals(ctx context.Context, tx *sql.Tx, g *workflowGraph, filter string, args []any, reason string, now time.Time) error {
where := ` WHERE s.namespace_id=? AND s.workflow_id=? AND s.state='PENDING'` + filter
params := append([]any{g.view.NamespaceID, g.view.ID}, args...)
rows, err := tx.QueryContext(ctx, `SELECT s.id,s.revision FROM weave_signals s`+where+` ORDER BY s.id LIMIT ? FOR UPDATE`, append(params, signal.MaxPerWorkflow+1)...)
if err != nil {
return err
}
type entry struct {
id core.ID
revision uint64
}
var items []entry
for rows.Next() {
var v entry
if err = rows.Scan(&v.id, &v.revision); err != nil {
rows.Close()
return err
}
items = append(items, v)
}
err = rows.Err()
rows.Close()
if err != nil {
return err
}
if len(items) > signal.MaxPerWorkflow {
return errors.New("workflow signal limit violated")
}
if len(items) == 0 {
return nil
}
update := append([]any{reason, now}, params...)
result, err := tx.ExecContext(ctx, `UPDATE weave_signals s SET s.state='REJECTED',s.disposition=?,s.revision=s.revision+1,s.updated_at=?`+where, update...)
if err != nil {
return err
}
n, err := result.RowsAffected()
if err != nil {
return err
}
if n != int64(len(items)) {
return errors.New("workflow signal rejection set changed")
}
for start := 0; start < len(items); start += 32 {
end := start + 32
if end > len(items) {
end = len(items)
}
var values []string
var insert []any
for _, v := range items[start:end] {
event, err := core.NewID()
if err != nil {
return err
}
raw, err := json.Marshal(signalTransition{WorkflowID: g.view.ID, SignalID: v.id, From: "PENDING", State: "REJECTED", Disposition: reason})
if err != nil {
return err
}
values = append(values, "(?,?,?,?,?,?,?,UTC_TIMESTAMP(6))")
insert = append(insert, g.view.NamespaceID, event, "workflow_signal", v.id, v.revision+1, "workflow.signal.changed", string(raw))
}
if _, err = tx.ExecContext(ctx, `INSERT INTO orbit_events(namespace_id,id,aggregate_kind,aggregate_id,aggregate_revision,type,payload,occurred_at) VALUES `+strings.Join(values, ","), insert...); err != nil {
return err
}
}
return nil
}