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 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` 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 }