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

207 lines
7.8 KiB
Go

package mysql
import (
"context"
"database/sql"
"errors"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
"git.yuntuo.hk/k2safe/orbit/pulse/service"
"git.yuntuo.hk/k2safe/orbit/weave/definition"
"git.yuntuo.hk/k2safe/orbit/weave/engine"
"git.yuntuo.hk/k2safe/orbit/weave/instance"
)
type workflowHistoryParent struct {
view instance.View
gate core.ID
version definition.VersionView
step instance.StepView
node definition.Step
}
func readWorkflowHistoryParent(ctx context.Context, tx *sql.Tx, ns, id core.ID, key string, p auth.Permissions) (workflowHistoryParent, error) {
var h workflowHistoryParent
var err error
h.view, h.gate, h.version, err = readWorkflowForQuery(ctx, tx, ns, id, p)
if err != nil {
return h, err
}
steps, err := readWorkflowStepViews(ctx, tx, ns, id, h.gate, h.version, key)
if err != nil {
return h, err
}
if len(steps) != 1 {
return h, fault.NotFound
}
h.step = steps[0]
program, err := engine.Compile(h.version.Specification)
if err != nil {
return h, err
}
h.node, _ = program.Step(key)
return h, nil
}
const activationHistorySelect = `SELECT a.id,a.step_id,a.sequence,a.state,a.revision,a.task_id,a.created_at,a.updated_at,a.completed_at,t.id,t.queue_id FROM weave_activations a LEFT JOIN pulse_tasks t ON t.namespace_id=a.namespace_id AND t.id=a.task_id AND t.owner_kind='workflow_activation' AND t.owner_id=a.id AND t.gate_id=? WHERE a.namespace_id=? AND a.workflow_id=? AND a.step_id=?`
func scanWorkflowActivation(row scanner, h workflowHistoryParent) (instance.ActivationSummary, error) {
var a instance.ActivationSummary
var task, queue *core.ID
err := row.Scan(&a.ID, &a.StepID, &a.Sequence, &a.State, &a.Revision, &a.TaskID, &a.CreatedAt, &a.UpdatedAt, &a.CompletedAt, &task, &queue)
if err != nil {
return a, err
}
if a.StepID != h.step.ID || a.Sequence == 0 || (h.node.Task != nil) != (a.TaskID != nil) {
return a, fault.Unavailable
}
if a.TaskID != nil && (task == nil || queue == nil || *task != *a.TaskID || *queue != h.node.Task.QueueID) {
return a, fault.Unavailable
}
return a, nil
}
func readHistoryActivation(ctx context.Context, tx *sql.Tx, h workflowHistoryParent, activation core.ID) (instance.ActivationSummary, error) {
a, err := scanWorkflowActivation(tx.QueryRowContext(ctx, activationHistorySelect+` AND a.id=?`, h.gate, h.view.NamespaceID, h.view.ID, h.step.ID, activation), h)
if errors.Is(err, sql.ErrNoRows) {
return a, fault.NotFound
}
return a, err
}
func (s *WorkflowInstanceStore) ListActivations(ctx context.Context, t auth.Token, id core.ID, key string, in service.HistoryPageInput) (instance.ActivationPage, error) {
out := instance.ActivationPage{InstanceID: id, Items: []instance.ActivationSummary{}}
err := s.transaction(ctx, t, instance.ReadAction, func(tx *sql.Tx, p auth.Permissions) error {
h, err := readWorkflowHistoryParent(ctx, tx, t.NamespaceID, id, key, p)
if err != nil {
return err
}
out.Revision, out.StepID, out.CurrentActivationID = h.view.Revision, h.step.ID, h.step.ActivationID
q := activationHistorySelect
args := []any{h.gate, t.NamespaceID, id, h.step.ID}
if in.Cursor != "" {
c, err := decodeHistoryCursorKind(in.Cursor, t.NamespaceID, id, h.step.ID, "workflow-activations")
if err != nil {
return err
}
q += ` AND a.sequence<?`
args = append(args, c.Epoch)
}
q += ` ORDER BY a.sequence DESC LIMIT ?`
args = append(args, in.Limit+1)
rows, err := tx.QueryContext(ctx, q, args...)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
a, err := scanWorkflowActivation(rows, h)
if err != nil {
return err
}
out.Items = append(out.Items, a)
}
if err = rows.Err(); err != nil {
return err
}
if len(out.Items) > in.Limit {
out.Items = out.Items[:in.Limit]
last := out.Items[len(out.Items)-1]
out.NextCursor, err = encodeHistoryCursor(historyCursor{Version: 1, Kind: "workflow-activations", Namespace: t.NamespaceID, Task: id, Attempt: h.step.ID, Epoch: uint64(last.Sequence)})
}
return err
})
if err != nil {
return instance.ActivationPage{}, err
}
return out, nil
}
func (s *WorkflowInstanceStore) GetActivation(ctx context.Context, t auth.Token, id core.ID, key string, activation core.ID) (instance.ActivationDetail, error) {
out := instance.ActivationDetail{InstanceID: id}
err := s.transaction(ctx, t, instance.ReadAction, func(tx *sql.Tx, p auth.Permissions) error {
h, err := readWorkflowHistoryParent(ctx, tx, t.NamespaceID, id, key, p)
if err != nil {
return err
}
out.Revision, out.Step = h.view.Revision, h.step
out.Activation, err = readHistoryActivation(ctx, tx, h, activation)
if err != nil {
return err
}
var input, output []byte
if err = tx.QueryRowContext(ctx, `SELECT input,output FROM weave_activations WHERE namespace_id=? AND workflow_id=? AND step_id=? AND id=?`, t.NamespaceID, id, h.step.ID, activation).Scan(&input, &output); err != nil {
return err
}
out.Input, out.Output = input, output
out.Poll, out.Wait, err = readWorkflowActivationAttachments(ctx, tx, t.NamespaceID, id, h.gate, key, string(h.step.Kind), activation, out.Activation.Sequence, h.version)
return err
})
if err != nil {
return instance.ActivationDetail{}, err
}
return out, nil
}
func (s *WorkflowInstanceStore) ListPollRounds(ctx context.Context, t auth.Token, id core.ID, key string, activation core.ID, in service.HistoryPageInput) (instance.PollRoundPage, error) {
out := instance.PollRoundPage{InstanceID: id, ActivationID: activation, Items: []instance.PollRoundSummary{}}
err := s.transaction(ctx, t, instance.ReadAction, func(tx *sql.Tx, p auth.Permissions) error {
h, err := readWorkflowHistoryParent(ctx, tx, t.NamespaceID, id, key, p)
if err != nil {
return err
}
a, err := readHistoryActivation(ctx, tx, h, activation)
if err != nil {
return err
}
if h.node.Poll == nil {
return fault.Invalid("round history requires a POLL activation")
}
out.Revision = h.view.Revision
poll, _, err := readWorkflowActivationAttachments(ctx, tx, t.NamespaceID, id, h.gate, key, string(h.step.Kind), activation, a.Sequence, h.version)
if err != nil {
return err
}
q := "SELECT r.id,r.activation_id,r.`round`,r.task_id,r.created_at,t.id,t.queue_id FROM weave_poll_rounds r LEFT JOIN pulse_tasks t ON t.namespace_id=r.namespace_id AND t.id=r.task_id AND t.owner_kind='workflow_activation' AND t.owner_id=r.activation_id AND t.gate_id=? WHERE r.namespace_id=? AND r.workflow_id=? AND r.activation_id=?"
args := []any{h.gate, t.NamespaceID, id, activation}
if in.Cursor != "" {
c, err := decodeHistoryCursorKind(in.Cursor, t.NamespaceID, id, activation, "workflow-rounds")
if err != nil {
return err
}
q += " AND r.`round`<?"
args = append(args, c.Epoch)
}
q += " ORDER BY r.`round` DESC LIMIT ?"
args = append(args, in.Limit+1)
rows, err := tx.QueryContext(ctx, q, args...)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var round instance.PollRoundSummary
var task, queue *core.ID
if err = rows.Scan(&round.ID, &round.ActivationID, &round.Round, &round.TaskID, &round.CreatedAt, &task, &queue); err != nil {
return err
}
if task == nil || queue == nil || *task != round.TaskID || *queue != h.node.Poll.Task.QueueID || round.Round < poll.FirstRound || round.Round > poll.UsedRounds {
return fault.Unavailable
}
out.Items = append(out.Items, round)
}
if err = rows.Err(); err != nil {
return err
}
if len(out.Items) > in.Limit {
out.Items = out.Items[:in.Limit]
last := out.Items[len(out.Items)-1]
out.NextCursor, err = encodeHistoryCursor(historyCursor{Version: 1, Kind: "workflow-rounds", Namespace: t.NamespaceID, Task: id, Attempt: activation, Epoch: uint64(last.Round)})
}
return err
})
if err != nil {
return instance.PollRoundPage{}, err
}
return out, nil
}