mirror of
https://github.com/hanzo-fi/stack.git
synced 2026-10-30 20:34:43 +00:00
377 lines
11 KiB
Go
377 lines
11 KiB
Go
package workflow
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/formancehq/stack/libs/go-libs/api"
|
|
"github.com/formancehq/stack/libs/go-libs/bun/bunpaginate"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/uptrace/bun"
|
|
"go.temporal.io/api/enums/v1"
|
|
"go.temporal.io/api/history/v1"
|
|
"go.temporal.io/api/serviceerror"
|
|
"go.temporal.io/sdk/client"
|
|
)
|
|
|
|
var (
|
|
ErrInstanceNotFound = errors.New("Instance not found")
|
|
ErrWorkflowNotFound = errors.New("Workflow not found")
|
|
)
|
|
|
|
const (
|
|
EventSignalName = "event"
|
|
)
|
|
|
|
type Event struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type WorkflowManager struct {
|
|
db *bun.DB
|
|
temporalClient client.Client
|
|
taskQueue string
|
|
}
|
|
|
|
func (m *WorkflowManager) Create(ctx context.Context, config Config) (*Workflow, error) {
|
|
|
|
if err := config.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
workflow := New(config)
|
|
|
|
if _, err := m.db.
|
|
NewInsert().
|
|
Model(&workflow).
|
|
Exec(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &workflow, nil
|
|
}
|
|
|
|
func (m *WorkflowManager) DeleteWorkflow(ctx context.Context, id string) error {
|
|
|
|
var workflow Workflow
|
|
|
|
res, err := m.db.NewUpdate().Model(&workflow).Where("id = ?", id).Set("deleted_at = ?", time.Now()).Exec(ctx)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r, err := res.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if r == 0 {
|
|
return ErrWorkflowNotFound
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *WorkflowManager) RunWorkflow(ctx context.Context, id string, variables map[string]string) (*Instance, error) {
|
|
|
|
workflow := Workflow{}
|
|
if err := m.db.NewSelect().
|
|
Where("id = ?", id).
|
|
Model(&workflow).
|
|
Scan(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
instance := NewInstance(id)
|
|
|
|
if _, err := m.db.
|
|
NewInsert().
|
|
Model(&instance).
|
|
Exec(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err := m.temporalClient.ExecuteWorkflow(ctx, client.StartWorkflowOptions{
|
|
ID: instance.ID,
|
|
TaskQueue: m.taskQueue,
|
|
}, Run, Input{
|
|
Instance: instance,
|
|
Workflow: workflow,
|
|
Variables: variables,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &instance, nil
|
|
}
|
|
|
|
func (m *WorkflowManager) Wait(ctx context.Context, instanceID string) error {
|
|
if err := m.temporalClient.
|
|
GetWorkflow(ctx, instanceID, "").
|
|
Get(ctx, nil); err != nil {
|
|
if errors.Is(err, &serviceerror.NotFound{}) {
|
|
return ErrInstanceNotFound
|
|
}
|
|
return errors.Unwrap(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *WorkflowManager) ListWorkflows(ctx context.Context, query bunpaginate.OffsetPaginatedQuery[any]) (*api.Cursor[Workflow], error) {
|
|
workflows := make([]Workflow, 0)
|
|
sb := m.db.NewSelect().
|
|
Model(&workflows).
|
|
Where("deleted_at IS NULL")
|
|
|
|
return bunpaginate.UsingOffset[any, Workflow](ctx, sb, query)
|
|
}
|
|
|
|
func (m *WorkflowManager) ReadWorkflow(ctx context.Context, id string) (Workflow, error) {
|
|
var workflow Workflow
|
|
if err := m.db.NewSelect().
|
|
Model(&workflow).
|
|
Where("id = ?", id).
|
|
Scan(ctx); err != nil {
|
|
return Workflow{}, err
|
|
}
|
|
return workflow, nil
|
|
}
|
|
|
|
func (m *WorkflowManager) PostEvent(ctx context.Context, instanceID string, event Event) error {
|
|
stage := Stage{}
|
|
if err := m.db.NewSelect().
|
|
Model(&stage).
|
|
Where("instance_id = ?", instanceID).
|
|
Limit(1).
|
|
OrderExpr("stage desc").
|
|
Scan(ctx); err != nil {
|
|
return errors.Wrap(err, "retrieving workflow")
|
|
}
|
|
|
|
err := m.temporalClient.SignalWorkflow(ctx, stage.TemporalWorkflowID(), "", EventSignalName, event)
|
|
if err != nil {
|
|
return errors.Wrap(err, "sending signal to server")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *WorkflowManager) AbortRun(ctx context.Context, instanceID string) error {
|
|
instance := Instance{}
|
|
if err := m.db.NewSelect().
|
|
Model(&instance).
|
|
Where("id = ?", instanceID).
|
|
Scan(ctx); err != nil {
|
|
return errors.Wrap(err, "retrieving workflow execution")
|
|
}
|
|
|
|
return m.temporalClient.CancelWorkflow(ctx, instanceID, "")
|
|
}
|
|
|
|
func (m *WorkflowManager) ListInstances(ctx context.Context, pagination ListInstancesQuery) (*api.Cursor[Instance], error) {
|
|
instances := make([]Instance, 0)
|
|
query := m.db.NewSelect().Model(&instances).
|
|
Join("JOIN workflows ON workflows.id = u.workflow_id").
|
|
Where("workflows.deleted_at IS NULL")
|
|
|
|
if pagination.Options.WorkflowID != "" {
|
|
query = query.Where("workflows.id = ?", pagination.Options.WorkflowID)
|
|
}
|
|
if pagination.Options.Running {
|
|
query = query.Where("u.terminated = false")
|
|
}
|
|
|
|
return bunpaginate.UsingOffset[ListInstancesOptions, Instance](ctx, query, bunpaginate.OffsetPaginatedQuery[ListInstancesOptions](pagination))
|
|
}
|
|
|
|
type StageHistory struct {
|
|
Name string `json:"name"`
|
|
Input map[string]any `json:"input"`
|
|
Error string `json:"error,omitempty"`
|
|
Terminated bool `json:"terminated"`
|
|
StartedAt time.Time `json:"startedAt"`
|
|
TerminatedAt *time.Time `json:"terminatedAt,omitempty"`
|
|
}
|
|
|
|
func (m *WorkflowManager) ReadInstanceHistory(ctx context.Context, instanceID string) ([]StageHistory, error) {
|
|
historyIterator := m.temporalClient.GetWorkflowHistory(ctx, instanceID, "",
|
|
false, enums.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT)
|
|
ret := make([]StageHistory, 0)
|
|
for historyIterator.HasNext() {
|
|
event, err := historyIterator.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch event.EventType {
|
|
case enums.EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED:
|
|
attributes := event.Attributes.(*history.HistoryEvent_StartChildWorkflowExecutionInitiatedEventAttributes)
|
|
input := make(map[string]any)
|
|
if err := json.Unmarshal(attributes.StartChildWorkflowExecutionInitiatedEventAttributes.Input.Payloads[0].Data, &input); err != nil {
|
|
panic(err)
|
|
}
|
|
stageHistory := StageHistory{
|
|
Name: attributes.StartChildWorkflowExecutionInitiatedEventAttributes.WorkflowType.Name,
|
|
Input: input,
|
|
StartedAt: *event.EventTime,
|
|
}
|
|
for historyIterator.HasNext() {
|
|
event, err = historyIterator.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch event.EventType {
|
|
case enums.EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_TERMINATED:
|
|
case enums.EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_FAILED:
|
|
attributes := event.Attributes.(*history.HistoryEvent_ChildWorkflowExecutionFailedEventAttributes).
|
|
ChildWorkflowExecutionFailedEventAttributes
|
|
stageHistory.Error = attributes.Failure.Message
|
|
case enums.EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_COMPLETED:
|
|
case enums.EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_TIMED_OUT:
|
|
stageHistory.Error = "timeout"
|
|
case enums.EVENT_TYPE_CHILD_WORKFLOW_EXECUTION_CANCELED:
|
|
stageHistory.Error = "canceled"
|
|
default:
|
|
continue
|
|
}
|
|
stageHistory.TerminatedAt = event.EventTime
|
|
stageHistory.Terminated = true
|
|
break
|
|
}
|
|
ret = append(ret, stageHistory)
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
type ActivityHistory struct {
|
|
Name string `json:"name"`
|
|
Input map[string]any `json:"input"`
|
|
Output map[string]any `json:"output,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Terminated bool `json:"terminated"`
|
|
StartedAt time.Time `json:"startedAt"`
|
|
TerminatedAt *time.Time `json:"terminatedAt,omitempty"`
|
|
LastFailure string `json:"lastFailure,omitempty"`
|
|
Attempt int `json:"attempt"`
|
|
NextExecution *time.Time `json:"nextExecution,omitempty"`
|
|
}
|
|
|
|
func (m *WorkflowManager) ReadStageHistory(ctx context.Context, instanceID string, stage int) ([]*ActivityHistory, error) {
|
|
stageID := fmt.Sprintf("%s-%d", instanceID, stage)
|
|
described, err := m.temporalClient.DescribeWorkflowExecution(ctx, stageID, "")
|
|
if err != nil {
|
|
if _, ok := err.(*serviceerror.NotFound); ok {
|
|
return nil, ErrInstanceNotFound
|
|
}
|
|
panic(err)
|
|
}
|
|
|
|
historyIterator := m.temporalClient.GetWorkflowHistory(ctx, stageID, "",
|
|
false, enums.HISTORY_EVENT_FILTER_TYPE_ALL_EVENT)
|
|
ret := make([]*ActivityHistory, 0)
|
|
for historyIterator.HasNext() {
|
|
event, err := historyIterator.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch event.EventType {
|
|
case enums.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED:
|
|
activityTaskScheduledEventAttributes := event.Attributes.(*history.HistoryEvent_ActivityTaskScheduledEventAttributes).ActivityTaskScheduledEventAttributes
|
|
input := make(map[string]any)
|
|
if err := json.Unmarshal(activityTaskScheduledEventAttributes.Input.Payloads[0].Data, &input); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
activityHistory := &ActivityHistory{
|
|
Name: activityTaskScheduledEventAttributes.ActivityType.Name,
|
|
Input: map[string]any{
|
|
activityTaskScheduledEventAttributes.ActivityType.Name: input,
|
|
},
|
|
StartedAt: *event.EventTime,
|
|
Attempt: 1,
|
|
}
|
|
ret = append(ret, activityHistory)
|
|
|
|
if len(described.PendingActivities) > 0 &&
|
|
activityTaskScheduledEventAttributes.ActivityId == described.PendingActivities[0].ActivityId {
|
|
pendingActivity := described.PendingActivities[0]
|
|
if pendingActivity.LastFailure != nil {
|
|
activityHistory.LastFailure = pendingActivity.LastFailure.Message
|
|
}
|
|
activityHistory.Attempt = int(pendingActivity.Attempt)
|
|
activityHistory.NextExecution = pendingActivity.ScheduledTime
|
|
return ret, nil
|
|
}
|
|
|
|
for historyIterator.HasNext() {
|
|
event, err = historyIterator.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch event.EventType {
|
|
case enums.EVENT_TYPE_ACTIVITY_TASK_CANCELED:
|
|
activityHistory.Error = "cancelled"
|
|
case enums.EVENT_TYPE_ACTIVITY_TASK_COMPLETED:
|
|
result := event.Attributes.(*history.HistoryEvent_ActivityTaskCompletedEventAttributes).ActivityTaskCompletedEventAttributes.Result
|
|
if result != nil && len(result.Payloads) > 0 {
|
|
output := make(map[string]any)
|
|
if err := json.Unmarshal(result.Payloads[0].Data, &output); err != nil {
|
|
panic(err)
|
|
}
|
|
activityHistory.Output = map[string]any{
|
|
activityTaskScheduledEventAttributes.ActivityType.Name: output,
|
|
}
|
|
}
|
|
case enums.EVENT_TYPE_ACTIVITY_TASK_TIMED_OUT:
|
|
activityHistory.Error = "timeout"
|
|
case enums.EVENT_TYPE_ACTIVITY_TASK_FAILED:
|
|
activityHistory.Error = event.Attributes.(*history.HistoryEvent_ActivityTaskFailedEventAttributes).
|
|
ActivityTaskFailedEventAttributes.Failure.Message
|
|
default:
|
|
continue
|
|
}
|
|
activityHistory.TerminatedAt = event.EventTime
|
|
activityHistory.Terminated = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (m *WorkflowManager) GetInstance(ctx context.Context, instanceID string) (*Instance, error) {
|
|
occurrence := Instance{}
|
|
err := m.db.NewSelect().
|
|
Model(&occurrence).
|
|
Relation("Statuses").
|
|
Where("id = ?", instanceID).
|
|
Scan(ctx)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, ErrInstanceNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return &occurrence, nil
|
|
}
|
|
|
|
func NewManager(db *bun.DB, temporalClient client.Client, taskQueue string) *WorkflowManager {
|
|
return &WorkflowManager{
|
|
db: db,
|
|
temporalClient: temporalClient,
|
|
taskQueue: taskQueue,
|
|
}
|
|
}
|
|
|
|
type ListInstancesOptions struct {
|
|
WorkflowID string
|
|
Running bool
|
|
}
|
|
|
|
type ListInstancesQuery bunpaginate.OffsetPaginatedQuery[ListInstancesOptions]
|