From da1d57f10033caf86aec5f40aa22c49c0f8912ef Mon Sep 17 00:00:00 2001 From: = Date: Thu, 16 Oct 2025 12:45:24 -0400 Subject: [PATCH 1/6] feat: trigger workflows --- cmd/smallflow/main.go | 72 ++++++++- go.mod | 2 + go.sum | 2 + internal/core/action.go | 22 +++ internal/core/adapters/clock.go | 7 + internal/core/adapters/es.go | 26 +++ internal/core/adapters/run_repo.go | 44 ++++++ internal/core/adapters/workflow_repo.go | 64 ++++++++ internal/core/api.go | 174 +++++++++++++++++++++ internal/core/run.go | 200 ++++++++++++++++++++++++ internal/core/spi.go | 19 +++ internal/core/workflow.go | 103 ++++++++++++ internal/core/workflow_definition.go | 21 +++ internal/core/workflow_disable.go | 25 +++ internal/core/workflow_enable.go | 25 +++ internal/core/workflow_run.go | 88 +++++++++++ internal/core/workflow_trigger.go | 37 +++++ internal/orchestrator/orchestrator.go | 27 ++++ 18 files changed, 957 insertions(+), 1 deletion(-) create mode 100644 go.sum create mode 100644 internal/core/action.go create mode 100644 internal/core/adapters/clock.go create mode 100644 internal/core/adapters/es.go create mode 100644 internal/core/adapters/run_repo.go create mode 100644 internal/core/adapters/workflow_repo.go create mode 100644 internal/core/api.go create mode 100644 internal/core/run.go create mode 100644 internal/core/spi.go create mode 100644 internal/core/workflow.go create mode 100644 internal/core/workflow_definition.go create mode 100644 internal/core/workflow_disable.go create mode 100644 internal/core/workflow_enable.go create mode 100644 internal/core/workflow_run.go create mode 100644 internal/core/workflow_trigger.go create mode 100644 internal/orchestrator/orchestrator.go diff --git a/cmd/smallflow/main.go b/cmd/smallflow/main.go index facc4d1..57cd473 100644 --- a/cmd/smallflow/main.go +++ b/cmd/smallflow/main.go @@ -1,7 +1,77 @@ package main -import "fmt" +import ( + "context" + "fmt" + "github.com/morebec/smallflow/internal/core" + "github.com/morebec/smallflow/internal/core/adapters" + "github.com/morebec/smallflow/internal/orchestrator" +) func main() { fmt.Println("Build, run, and observe workflows without the overhead!") + + eventStore := &adapters.InMemoryEventStore{} + clock := adapters.RealTimeClock{} + workflowRepo := &adapters.EventStoreWorkflowRepository{ + EventStore: eventStore, + } + runRepo := &adapters.EventStoreRunRepository{ + EventStore: eventStore, + } + + api := core.NewAPI(clock, workflowRepo, runRepo) + orch := orchestrator.WorkflowOrchestrator{Clock: clock, API: api} + + ctx := context.Background() + + fmt.Println("Enabling workflow...") + if err := api.HandleCommand(ctx, core.EnableWorkflowCommand{ + WorkflowID: "my-workflow", + }); err != nil { + panic(err) + } + + fmt.Println("Triggering workflow...") + if err := api.HandleCommand(ctx, core.TriggerWorkflowCommand{ + WorkflowID: "my-workflow", + RunID: "run-1", + }); err != nil { + panic(err) + } + + fmt.Println("Triggering workflow...") + if err := api.HandleCommand(ctx, core.TriggerWorkflowCommand{ + WorkflowID: "my-workflow", + RunID: "run-1", + }); err != nil { + panic(err) + } + + fmt.Println("Disabling workflow...") + if err := api.HandleCommand(ctx, core.DisableWorkflowCommand{ + WorkflowID: "my-workflow", + }); err != nil { + panic(err) + } + + fmt.Println("Dispatching events through the orchestrator...") + for _, event := range eventStore.Events() { + if err := orch.HandleEvent(ctx, event); err != nil { + panic(err) + } + } + + fmt.Println("Current events in the event store:") + for i, event := range eventStore.Events() { + fmt.Printf("Event %d: %T → %+v\n", i, event, event) + } +} + +func registerActions() { + //actionRegistry := definition.ActionRegistry{} + //actionRegistry.Register(definition.NewActionFunc("my_action", func(ctx definition.Action) *definition.ActionError { + // fmt.Println("Hello, World!") + // return nil + //})) } diff --git a/go.mod b/go.mod index c9c1e0d..d2bd118 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/morebec/smallflow go 1.24.8 + +require github.com/alitto/pond/v2 v2.5.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..bac317e --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/alitto/pond/v2 v2.5.0 h1:vPzS5GnvSDRhWQidmj2djHllOmjFExVFbDGCw1jdqDw= +github.com/alitto/pond/v2 v2.5.0/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE= diff --git a/internal/core/action.go b/internal/core/action.go new file mode 100644 index 0000000..1761c3f --- /dev/null +++ b/internal/core/action.go @@ -0,0 +1,22 @@ +package core + +import "context" + +type ActionID string + +type Action interface { + Run(ctx context.Context) *WorkflowError + ID() ActionID +} + +type ActionFunc struct { + fn func(ctx context.Context) *WorkflowError + id ActionID +} + +func (a *ActionFunc) Run(ctx context.Context) *WorkflowError { return a.fn(ctx) } +func (a *ActionFunc) ID() ActionID { return a.id } + +func NewActionFunc(id ActionID, fn func(ctx context.Context) *WorkflowError) *ActionFunc { + return &ActionFunc{id: id, fn: fn} +} diff --git a/internal/core/adapters/clock.go b/internal/core/adapters/clock.go new file mode 100644 index 0000000..cf4794d --- /dev/null +++ b/internal/core/adapters/clock.go @@ -0,0 +1,7 @@ +package adapters + +import "time" + +type RealTimeClock struct{} + +func (RealTimeClock) Now() time.Time { return time.Now() } diff --git a/internal/core/adapters/es.go b/internal/core/adapters/es.go new file mode 100644 index 0000000..db240a4 --- /dev/null +++ b/internal/core/adapters/es.go @@ -0,0 +1,26 @@ +package adapters + +import ( + "context" + "sync" +) + +type InMemoryEventStore struct { + mu sync.Mutex + events []any +} + +func (i *InMemoryEventStore) Record(ctx context.Context, event any) error { + i.mu.Lock() + defer i.mu.Unlock() + i.events = append(i.events, event) + return nil +} + +func (i *InMemoryEventStore) Events() []any { + i.mu.Lock() + defer i.mu.Unlock() + eventsCopy := make([]any, len(i.events)) + copy(eventsCopy, i.events) + return eventsCopy +} diff --git a/internal/core/adapters/run_repo.go b/internal/core/adapters/run_repo.go new file mode 100644 index 0000000..6f39cf4 --- /dev/null +++ b/internal/core/adapters/run_repo.go @@ -0,0 +1,44 @@ +package adapters + +import ( + "context" + "github.com/morebec/smallflow/internal/core" +) + +type EventStoreRunRepository struct { + EventStore *InMemoryEventStore +} + +func (r EventStoreRunRepository) Add(ctx context.Context, run *core.Run) error { + return r.Save(ctx, run) +} + +func (r EventStoreRunRepository) Save(ctx context.Context, run *core.Run) error { + for _, event := range run.UncommittedEvents() { + if err := r.EventStore.Record(ctx, event); err != nil { + return err + } + } + run.Commit() + + return nil +} + +func (r EventStoreRunRepository) FindByID(_ context.Context, workflowID string, runID string) (*core.Run, error) { + var run *core.Run + for _, event := range r.EventStore.events { + switch ev := event.(type) { + case core.WorkflowStartedEvent: + run = &core.Run{ + ID: core.RunID(runID), + WorkflowID: core.WorkflowID(workflowID), + } + if ev.RunID == runID && ev.WorkflowID == workflowID { + run.Apply([]any{ev}) + } + } + } + + return run, nil + +} diff --git a/internal/core/adapters/workflow_repo.go b/internal/core/adapters/workflow_repo.go new file mode 100644 index 0000000..a4711df --- /dev/null +++ b/internal/core/adapters/workflow_repo.go @@ -0,0 +1,64 @@ +package adapters + +import ( + "context" + "fmt" + "github.com/morebec/smallflow/internal/core" +) + +type EventStoreWorkflowRepository struct { + EventStore *InMemoryEventStore +} + +func (e EventStoreWorkflowRepository) FindByID(_ context.Context, workflowID string) (*core.Workflow, error) { + events := e.EventStore.Events() + wf := core.NewWorkflow(core.WorkflowDefinition{ + ID: core.WorkflowID(workflowID), + ConcurrencyLimit: core.ConcurrencyLimitNone, + Steps: []core.StepDefinition{ + { + ID: "step-1", + IgnoreError: false, + Action: core.NewActionFunc("my-action", func(ctx context.Context) *core.WorkflowError { + fmt.Println("Executing my-action") + return nil + }), + }, + { + ID: "step-2", + IgnoreError: false, + Action: core.NewActionFunc("my-action-2", func(ctx context.Context) *core.WorkflowError { + fmt.Println("Executing my-action 2") + return &core.WorkflowError{ + Kind: "internal", + Code: "not_implemented", + Message: "this action is not implemented", + Details: map[string]any{"action_id": "my-action-2"}, + } + //return nil + }), + }, + }, + }, false) + + for _, event := range events { + switch ev := event.(type) { + case core.WorkflowEnabledEvent, core.WorkflowDisabledEvent, core.WorkflowTriggeredEvent: + wf.Apply([]any{ev}) + } + } + + return wf, nil +} + +func (e EventStoreWorkflowRepository) Save(ctx context.Context, wf *core.Workflow) error { + for _, event := range wf.UncommittedEvents() { + if err := e.EventStore.Record(ctx, event); err != nil { + return err + } + } + + wf.Commit() + + return nil +} diff --git a/internal/core/api.go b/internal/core/api.go new file mode 100644 index 0000000..23bd032 --- /dev/null +++ b/internal/core/api.go @@ -0,0 +1,174 @@ +package core + +import ( + "context" + "fmt" + "time" +) + +type API struct { + EnableWorkflowCommandHandler EnableWorkflowCommandHandler + DisableWorkflowCommandHandler DisableWorkflowCommandHandler + TriggerWorkflowCommandHandler TriggerWorkflowCommandHandler + RunWorkflowCommandHandler RunWorkflowCommandHandler +} + +func NewAPI(clock Clock, workflowRepo WorkflowRepository, repo RunRepository) *API { + return &API{ + EnableWorkflowCommandHandler: EnableWorkflowCommandHandler{ + Clock: clock, + WorkflowRepository: workflowRepo, + }, + DisableWorkflowCommandHandler: DisableWorkflowCommandHandler{ + Clock: clock, + WorkflowRepository: workflowRepo, + }, + TriggerWorkflowCommandHandler: TriggerWorkflowCommandHandler{ + Clock: clock, + WorkflowRepository: workflowRepo, + }, + RunWorkflowCommandHandler: RunWorkflowCommandHandler{ + Clock: clock, + WorkflowRepository: workflowRepo, + RunRepository: repo, + }, + } +} + +func (api API) HandleCommand(ctx context.Context, cmd any) error { + switch c := cmd.(type) { + case EnableWorkflowCommand: + return api.EnableWorkflowCommandHandler.Handle(ctx, c) + case DisableWorkflowCommand: + return api.DisableWorkflowCommandHandler.Handle(ctx, c) + case TriggerWorkflowCommand: + return api.TriggerWorkflowCommandHandler.Handle(ctx, c) + case RunWorkflowCommand: + return api.RunWorkflowCommandHandler.Handle(ctx, c) + default: + return fmt.Errorf("unknown command type: %T", cmd) + } +} + +// TriggerWorkflowCommand represents a command to trigger a new run of a +// workflow. +// +// This command is idempotent based on the RunID provided. If a run +// with the same RunID already exists for the given workflow, this command will +// have no effect and will succeed. +// +// If no RunID is provided, a new unique RunID +// will be generated. +// +// The workflow must be enabled for this command to succeed. +// If the workflow is disabled, this command will fail. If the workflow does not +// exist, this command will fail. If the concurrent runs limit has been reached, +// this command will fail. +type TriggerWorkflowCommand struct { + WorkflowID string + RunID string +} + +type WorkflowTriggeredEvent struct { + WorkflowID string + RunID string + TriggeredAt time.Time +} + +// EnableWorkflowCommand represents a command to enable a workflow. Enabling a +// workflow allows new runs to be triggered. If the workflow does not exist, this +// command will fail. +type EnableWorkflowCommand struct { + WorkflowID string +} + +// WorkflowEnabledEvent is emitted when a workflow is successfully enabled. + +type WorkflowEnabledEvent struct { + WorkflowID string + EnabledAt time.Time +} + +// DisableWorkflowCommand represents a command to disable a workflow. Disabling a +// workflow prevents new runs from being triggered, but does not affect currently +// active runs. +type DisableWorkflowCommand struct { + WorkflowID string +} + +type WorkflowDisabledEvent struct { + WorkflowID string + DisabledAt time.Time + ActiveRuns int +} + +type WorkflowStartedEvent struct { + WorkflowID string + RunID string + StartedAt time.Time +} + +type WorkflowEndedEvent struct { + WorkflowID string + RunID string + EndedAt time.Time + StartedAt time.Time + Errors map[string]*WorkflowError + Status string +} + +type StepStartedEvent struct { + WorkflowID string + RunID string + StepID string + ActionID string + StartedAt time.Time + IgnoreErrors bool +} + +type StepEndedEvent struct { + WorkflowID string + RunID string + StepID string + ActionID string + EndedAt time.Time + Error *WorkflowError + Status string +} + +type WorkflowError struct { + Kind string // e.g. "user", "system", "internal" + Code string // e.g. "timeout", "network_error", "invalid_input" + Message string // human-readable message + Details map[string]any // additional details, e.g. {"go_error": "error message"} +} + +func (e WorkflowError) AsError() error { + return fmt.Errorf("%s(%s): %s", e.Kind, e.Code, e.Message) +} + +// RunWorkflowCommand runs a workflow synchronously. +// This command is idempotent based on the RunID provided: +// If a run with the same RunID already exists for the given workflow, this command will +// have no effect and will succeed. +// If no RunID is provided, this command will fail. +// This command is intended to be run after a workflow has been triggered. +// +// This command will not fail if the workflow or any of its steps fail, as these +// are expected outcomes of a workflow run. Instead, the errors will be recorded +// in the StepEndedEvent and WorkflowEndedEvent. +// This command will only return if internal errors have occurred, such as +// issues with the data storage. +type RunWorkflowCommand struct { + WorkflowID string + RunID string +} + +type WorkflowRunReport struct { + WorkflowID string + RunID string + StartedAt time.Time + EndedAt time.Time + Errors map[string]*WorkflowError + Status string +} diff --git a/internal/core/run.go b/internal/core/run.go new file mode 100644 index 0000000..682d9cd --- /dev/null +++ b/internal/core/run.go @@ -0,0 +1,200 @@ +package core + +import ( + "fmt" + "time" +) + +type WorkflowStatus string + +const ( + WorkflowStatusFailed WorkflowStatus = "failed" + WorkflowStatusSucceeded WorkflowStatus = "succeeded" +) + +type RunID string + +type Run struct { + ID RunID + WorkflowID WorkflowID + StartedAt time.Time + EndedAt *time.Time + Error *WorkflowError + CurrentStepID StepID + Steps map[StepID]*StepRun + + events []any + Status WorkflowStatus +} + +func StartRun(workflowID WorkflowID, id RunID, startedAt time.Time) *Run { + run := &Run{} + run.record(WorkflowStartedEvent{ + RunID: string(id), + WorkflowID: string(workflowID), + StartedAt: startedAt, + }) + return run +} + +func (r *Run) StartStep(stepID StepID, id ActionID, ignoreErrors bool, currentTime time.Time) error { + if r.CurrentStepID == stepID { + // already running, idempotent + return nil + } + + if r.CurrentStepID != "" { + return fmt.Errorf( + "workflow error: %s: cannot start step %s, step %s is already running", + r.WorkflowID, + stepID, + r.CurrentStepID, + ) + } + + r.record(StepStartedEvent{ + RunID: string(r.ID), + WorkflowID: string(r.WorkflowID), + StepID: string(stepID), + ActionID: string(id), + StartedAt: currentTime, + IgnoreErrors: ignoreErrors, + }) + + return nil +} + +func (r *Run) EndStep(stepID StepID, err *WorkflowError, currentTime time.Time) error { + step := r.Steps[stepID] + if step.EndedAt != nil { + // already ended, idempotent + return nil + } + + if r.CurrentStepID != stepID { + return fmt.Errorf( + "workflow error: %s: cannot end step %s, step %s is currently running", + r.WorkflowID, + stepID, + r.CurrentStepID, + ) + } + + status := StepStatusSucceeded + if err != nil && !step.IgnoreError { + status = StepStatusFailed + } + + r.record(StepEndedEvent{ + RunID: string(r.ID), + WorkflowID: string(r.WorkflowID), + StepID: string(stepID), + ActionID: string(step.ActionID), + EndedAt: currentTime, + Error: err, + Status: string(status), + }) + + return nil +} + +func (r *Run) End(currentTime time.Time) { + if r.EndedAt != nil { + // Already ended + return + } + + // Collect step errors + var stepErrors map[string]WorkflowError + workflowStatus := WorkflowStatusSucceeded + for _, s := range r.Steps { + if s.Error != nil { + if stepErrors == nil { + stepErrors = make(map[string]WorkflowError) + } + stepErrors[string(s.ID)] = *s.Error + } + if s.Status == StepStatusFailed { + workflowStatus = WorkflowStatusFailed + } + } + + // Compute Status + + r.record(WorkflowEndedEvent{ + RunID: string(r.ID), + WorkflowID: string(r.WorkflowID), + StartedAt: r.StartedAt, + EndedAt: currentTime, + Status: string(workflowStatus), + }) +} + +func (r *Run) Errors() map[string]*WorkflowError { + var stepErrors map[string]*WorkflowError + for _, s := range r.Steps { + if stepErrors == nil { + stepErrors = make(map[string]*WorkflowError) + } + stepErrors[string(s.ID)] = s.Error + } + return stepErrors +} + +func (r *Run) Apply(events []any) { + for _, event := range events { + switch e := event.(type) { + case WorkflowStartedEvent: + r.ID = RunID(e.RunID) + r.WorkflowID = WorkflowID(e.WorkflowID) + r.Steps = make(map[StepID]*StepRun) + r.StartedAt = e.StartedAt + + case WorkflowEndedEvent: + r.EndedAt = &e.EndedAt + r.Status = WorkflowStatus(e.Status) + + case StepStartedEvent: + r.CurrentStepID = StepID(e.StepID) + r.Steps[StepID(e.StepID)] = &StepRun{ + ID: StepID(e.StepID), + StartedAt: e.StartedAt, + ActionID: ActionID(e.ActionID), + IgnoreError: e.IgnoreErrors, + } + + case StepEndedEvent: + r.CurrentStepID = "" + step := r.Steps[StepID(e.StepID)] + step.EndedAt = &e.EndedAt + step.Error = e.Error + step.Status = StepStatus(e.Status) + } + } +} + +func (r *Run) UncommittedEvents() []any { return r.events } + +func (r *Run) record(event any) { + r.events = append(r.events, event) + r.Apply([]any{event}) +} + +func (r *Run) Commit() { r.events = nil } + +type StepStatus string + +const ( + StepStatusFailed StepStatus = "failed" + StepStatusSucceeded StepStatus = "succeeded" +) + +type StepRun struct { + ID StepID + StartedAt time.Time + EndedAt *time.Time + Error *WorkflowError + ActionID ActionID + Status StepStatus + IgnoreError bool +} diff --git a/internal/core/spi.go b/internal/core/spi.go new file mode 100644 index 0000000..927ac7a --- /dev/null +++ b/internal/core/spi.go @@ -0,0 +1,19 @@ +package core + +import ( + "context" + "time" +) + +type WorkflowRepository interface { + FindByID(ctx context.Context, workflowID string) (*Workflow, error) + Save(ctx context.Context, wf *Workflow) error +} + +type RunRepository interface { + FindByID(ctx context.Context, workflowID string, runID string) (*Run, error) + Add(ctx context.Context, run *Run) error + Save(ctx context.Context, r *Run) error +} + +type Clock interface{ Now() time.Time } diff --git a/internal/core/workflow.go b/internal/core/workflow.go new file mode 100644 index 0000000..a6bf5bd --- /dev/null +++ b/internal/core/workflow.go @@ -0,0 +1,103 @@ +package core + +import ( + "fmt" + "time" +) + +type WorkflowID string + +type Workflow struct { + enabled bool + definition WorkflowDefinition + + runIds map[RunID]struct{} + activeRuns map[RunID]struct{} + + events []any +} + +func NewWorkflow(d WorkflowDefinition, enabled bool) *Workflow { + return &Workflow{ + definition: d, + enabled: enabled, + runIds: make(map[RunID]struct{}), + activeRuns: make(map[RunID]struct{}), + } +} + +func (w *Workflow) ID() WorkflowID { return w.definition.ID } + +func (w *Workflow) Trigger(id RunID, currentTime time.Time) error { + if _, exists := w.runIds[id]; exists { + return nil // idempotent + } + + if !w.enabled { + return fmt.Errorf("workflow is not enabled: %s", w.ID()) + } + + if w.definition.ConcurrencyLimit != ConcurrencyLimitNone && + len(w.activeRuns) >= int(w.definition.ConcurrencyLimit) { + return fmt.Errorf("workflow concurrency limit reached: %s", w.ID()) + } + + w.record(WorkflowTriggeredEvent{ + WorkflowID: string(w.ID()), + RunID: string(id), + TriggeredAt: currentTime, + }) + + return nil +} + +func (w *Workflow) Enable(now time.Time) { + if w.enabled { + return + } + + w.record(WorkflowEnabledEvent{ + WorkflowID: string(w.ID()), + EnabledAt: now, + }) +} + +func (w *Workflow) Disable(now time.Time) { + if !w.enabled { + return + } + + w.record(WorkflowDisabledEvent{ + WorkflowID: string(w.ID()), + DisabledAt: now, + }) +} + +func (w *Workflow) record(event any) { + w.Apply([]any{event}) + w.events = append(w.events, event) +} + +func (w *Workflow) UncommittedEvents() []any { return w.events } + +func (w *Workflow) Apply(events []any) { + for _, event := range events { + switch e := event.(type) { + case WorkflowTriggeredEvent: + w.runIds[RunID(e.RunID)] = struct{}{} + w.activeRuns[RunID(e.RunID)] = struct{}{} + case WorkflowEndedEvent: + delete(w.activeRuns, RunID(e.RunID)) + + case WorkflowEnabledEvent: + w.enabled = true + + case WorkflowDisabledEvent: + w.enabled = false + } + } +} + +func (w *Workflow) Definition() WorkflowDefinition { return w.definition } + +func (w *Workflow) Commit() { w.events = nil } diff --git a/internal/core/workflow_definition.go b/internal/core/workflow_definition.go new file mode 100644 index 0000000..542e8cc --- /dev/null +++ b/internal/core/workflow_definition.go @@ -0,0 +1,21 @@ +package core + +type StepID string + +type StepDefinition struct { + ID StepID + IgnoreError bool + Action Action +} + +type ConcurrencyLimit int + +const ( + ConcurrencyLimitNone ConcurrencyLimit = 0 +) + +type WorkflowDefinition struct { + ID WorkflowID + ConcurrencyLimit ConcurrencyLimit + Steps []StepDefinition +} diff --git a/internal/core/workflow_disable.go b/internal/core/workflow_disable.go new file mode 100644 index 0000000..17f1778 --- /dev/null +++ b/internal/core/workflow_disable.go @@ -0,0 +1,25 @@ +package core + +import ( + "context" + "fmt" +) + +type DisableWorkflowCommandHandler struct { + Clock Clock + WorkflowRepository WorkflowRepository +} + +func (h DisableWorkflowCommandHandler) Handle(ctx context.Context, cmd DisableWorkflowCommand) error { + workflow, err := h.WorkflowRepository.FindByID(ctx, cmd.WorkflowID) + if err != nil { + return err + } + if workflow == nil { + return fmt.Errorf("workflow not found: %s", cmd.WorkflowID) + } + + workflow.Disable(h.Clock.Now()) + + return h.WorkflowRepository.Save(ctx, workflow) +} diff --git a/internal/core/workflow_enable.go b/internal/core/workflow_enable.go new file mode 100644 index 0000000..5ad45e5 --- /dev/null +++ b/internal/core/workflow_enable.go @@ -0,0 +1,25 @@ +package core + +import ( + "context" + "fmt" +) + +type EnableWorkflowCommandHandler struct { + Clock Clock + WorkflowRepository WorkflowRepository +} + +func (h EnableWorkflowCommandHandler) Handle(ctx context.Context, cmd EnableWorkflowCommand) error { + workflow, err := h.WorkflowRepository.FindByID(ctx, cmd.WorkflowID) + if err != nil { + return err + } + if workflow == nil { + return fmt.Errorf("workflow not found: %s", cmd.WorkflowID) + } + + workflow.Enable(h.Clock.Now()) + + return h.WorkflowRepository.Save(ctx, workflow) +} diff --git a/internal/core/workflow_run.go b/internal/core/workflow_run.go new file mode 100644 index 0000000..9c7d90e --- /dev/null +++ b/internal/core/workflow_run.go @@ -0,0 +1,88 @@ +package core + +import ( + "context" + "fmt" +) + +type RunWorkflowCommandHandler struct { + Clock Clock + WorkflowRepository WorkflowRepository + RunRepository RunRepository +} + +func (h RunWorkflowCommandHandler) Handle(ctx context.Context, cmd RunWorkflowCommand) error { + run, err := h.RunRepository.FindByID(ctx, cmd.WorkflowID, cmd.RunID) + if err != nil { + return err + } + if run != nil { + // Run already exists, nothing to do. + return nil + } + + wf, err := h.WorkflowRepository.FindByID(ctx, cmd.WorkflowID) + if err != nil { + return err + } + if wf == nil { + return fmt.Errorf("workflow not found: %s", cmd.WorkflowID) + } + + run = StartRun(WorkflowID(cmd.WorkflowID), RunID(cmd.RunID), h.Clock.Now()) + if err := h.RunRepository.Add(ctx, run); err != nil { + return err + } + + for _, step := range wf.Definition().Steps { + if err = h.runStep(ctx, run, step); err != nil { + break + } + } + + run.End(h.Clock.Now()) + if err := h.RunRepository.Save(ctx, run); err != nil { + return err + } + + if err != nil { + return fmt.Errorf("workflow run failed: %s: %w", run.ID, err) + } + + report := WorkflowRunReport{ + WorkflowID: string(run.WorkflowID), + RunID: string(run.ID), + StartedAt: run.StartedAt, + EndedAt: *run.EndedAt, + Errors: run.Errors(), + Status: string(run.Status), + } + + fmt.Printf("Workflow run completed: %+v \n", report) + + return nil +} + +func (h RunWorkflowCommandHandler) runStep(ctx context.Context, run *Run, step StepDefinition) error { + if err := run.StartStep(step.ID, step.Action.ID(), step.IgnoreError, h.Clock.Now()); err != nil { + return err + } + if err := h.RunRepository.Save(ctx, run); err != nil { + return err + } + + workflowErr := h.runStepAction(ctx, step) + + if err := run.EndStep(step.ID, workflowErr, h.Clock.Now()); err != nil { + return err + } + if err := h.RunRepository.Save(ctx, run); err != nil { + return err + } + + return nil +} + +func (h RunWorkflowCommandHandler) runStepAction(ctx context.Context, step StepDefinition) *WorkflowError { + return step.Action.Run(ctx) +} diff --git a/internal/core/workflow_trigger.go b/internal/core/workflow_trigger.go new file mode 100644 index 0000000..573ab87 --- /dev/null +++ b/internal/core/workflow_trigger.go @@ -0,0 +1,37 @@ +package core + +import ( + "context" + "fmt" +) + +type TriggerWorkflowCommandHandler struct { + WorkflowRepository WorkflowRepository + Clock Clock +} + +func (h TriggerWorkflowCommandHandler) Handle(ctx context.Context, cmd TriggerWorkflowCommand) error { + wf, err := h.WorkflowRepository.FindByID(ctx, cmd.WorkflowID) + if err != nil { + return err + } + if wf == nil { + return fmt.Errorf("workflow not found: %s", cmd.WorkflowID) + } + + if cmd.WorkflowID == "" { + return fmt.Errorf("workflow ID is required") + } + + if cmd.RunID == "" { + cmd.RunID = generateRunID() + } + + if err := wf.Trigger(RunID(cmd.RunID), h.Clock.Now()); err != nil { + return err + } + + return h.WorkflowRepository.Save(ctx, wf) +} + +func generateRunID() string { return "some-generated-run-id" } diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go new file mode 100644 index 0000000..46587a1 --- /dev/null +++ b/internal/orchestrator/orchestrator.go @@ -0,0 +1,27 @@ +package orchestrator + +import ( + "context" + "github.com/morebec/smallflow/internal/core" +) + +type WorkflowOrchestrator struct { + Clock core.Clock + API *core.API +} + +func (m WorkflowOrchestrator) HandleEvent(ctx context.Context, event any) error { + switch e := event.(type) { + case core.WorkflowTriggeredEvent: + return m.runWorkflow(ctx, e) + } + + return nil +} + +func (m WorkflowOrchestrator) runWorkflow(ctx context.Context, e core.WorkflowTriggeredEvent) error { + return m.API.HandleCommand(ctx, core.RunWorkflowCommand{ + WorkflowID: e.WorkflowID, + RunID: e.RunID, + }) +} From fb3e17b80f3710d221bc8f45a88875f0289f9fa8 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 17 Oct 2025 19:58:04 -0400 Subject: [PATCH 2/6] Add misass an mx --- cmd/smallflow/main.go | 58 +++++++---- go.mod | 15 ++- go.sum | 42 ++++++++ internal/core/adapters/clock.go | 7 -- internal/core/adapters/es.go | 3 +- internal/core/adapters/run_repo.go | 7 +- internal/core/adapters/workflow_repo.go | 5 +- internal/core/api.go | 123 ++++++++++++++++------- internal/core/run.go | 18 ++-- internal/core/runner.go | 128 ++++++++++++++++++++++++ internal/core/spi.go | 14 ++- internal/core/util.go | 19 ++++ internal/core/workflow.go | 11 +- internal/core/workflow_disable.go | 13 +-- internal/core/workflow_enable.go | 13 +-- internal/core/workflow_resume.go | 30 ++++++ internal/core/workflow_run.go | 77 +++----------- internal/core/workflow_trigger.go | 25 ++--- internal/orchestrator/adapters/lease.go | 37 +++++++ internal/orchestrator/lease.go | 72 +++++++++++++ internal/orchestrator/orchestrator.go | 43 ++++++-- internal/orchestrator/runner.go | 89 ++++++++++++++++ 22 files changed, 662 insertions(+), 187 deletions(-) delete mode 100644 internal/core/adapters/clock.go create mode 100644 internal/core/runner.go create mode 100644 internal/core/util.go create mode 100644 internal/core/workflow_resume.go create mode 100644 internal/orchestrator/adapters/lease.go create mode 100644 internal/orchestrator/lease.go create mode 100644 internal/orchestrator/runner.go diff --git a/cmd/smallflow/main.go b/cmd/smallflow/main.go index 57cd473..bb70f74 100644 --- a/cmd/smallflow/main.go +++ b/cmd/smallflow/main.go @@ -3,16 +3,21 @@ package main import ( "context" "fmt" + "github.com/alitto/pond/v2" + "github.com/morebec/go-misas/muuid" + "github.com/morebec/go-misas/mx" "github.com/morebec/smallflow/internal/core" "github.com/morebec/smallflow/internal/core/adapters" "github.com/morebec/smallflow/internal/orchestrator" + adapters2 "github.com/morebec/smallflow/internal/orchestrator/adapters" + "time" ) func main() { fmt.Println("Build, run, and observe workflows without the overhead!") eventStore := &adapters.InMemoryEventStore{} - clock := adapters.RealTimeClock{} + clock := mx.NewRealTimeClock(time.UTC) workflowRepo := &adapters.EventStoreWorkflowRepository{ EventStore: eventStore, } @@ -20,48 +25,59 @@ func main() { EventStore: eventStore, } - api := core.NewAPI(clock, workflowRepo, runRepo) - orch := orchestrator.WorkflowOrchestrator{Clock: clock, API: api} + api := core.NewSubsystem(clock, workflowRepo, runRepo, muuid.NewRandomUUIDGenerator()).API + orch := &orchestrator.WorkflowOrchestrator{ + Clock: clock, + API: api, + LeaseManager: orchestrator.WorkflowLeaseManager{ + Clock: clock, + Repository: adapters2.NewInMemoryWorkflowLeaseRepository(), + }, + Pool: pond.NewPool(10), + } ctx := context.Background() fmt.Println("Enabling workflow...") - if err := api.HandleCommand(ctx, core.EnableWorkflowCommand{ - WorkflowID: "my-workflow", - }); err != nil { - panic(err) - } - - fmt.Println("Triggering workflow...") - if err := api.HandleCommand(ctx, core.TriggerWorkflowCommand{ + if result := api.HandleCommand(ctx, core.EnableWorkflowCommand{ WorkflowID: "my-workflow", - RunID: "run-1", - }); err != nil { - panic(err) + }); result.Error != nil { + panic(result.Error) } fmt.Println("Triggering workflow...") - if err := api.HandleCommand(ctx, core.TriggerWorkflowCommand{ + if result := api.HandleCommand(ctx, core.TriggerWorkflowCommand{ WorkflowID: "my-workflow", - RunID: "run-1", - }); err != nil { - panic(err) + RunID: muuid.NewRandomUUIDGenerator().Generate().String(), + }); result.Error != nil { + panic(result.Error) } fmt.Println("Disabling workflow...") - if err := api.HandleCommand(ctx, core.DisableWorkflowCommand{ + if result := api.HandleCommand(ctx, core.DisableWorkflowCommand{ WorkflowID: "my-workflow", - }); err != nil { - panic(err) + }); result.Error != nil { + panic(result.Error) } fmt.Println("Dispatching events through the orchestrator...") + orch.Start() + defer orch.Stop() + for _, event := range eventStore.Events() { if err := orch.HandleEvent(ctx, event); err != nil { panic(err) } } + for orch.IsRunning() { + select { + case <-time.After(15 * time.Second): + fmt.Println("Stopping orchestrator after 15 seconds...") + orch.Stop() + } + } + fmt.Println("Current events in the event store:") for i, event := range eventStore.Events() { fmt.Printf("Event %d: %T → %+v\n", i, event, event) diff --git a/go.mod b/go.mod index d2bd118..689f4c0 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,17 @@ module github.com/morebec/smallflow go 1.24.8 -require github.com/alitto/pond/v2 v2.5.0 // indirect +replace github.com/morebec/go-misas => ./../go-misas-back + +require ( + github.com/alitto/pond/v2 v2.5.0 + github.com/morebec/go-misas v0.0.0-00010101000000-000000000000 +) + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/samber/lo v1.49.1 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/text v0.24.0 // indirect +) diff --git a/go.sum b/go.sum index bac317e..d4ccfad 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,44 @@ github.com/alitto/pond/v2 v2.5.0 h1:vPzS5GnvSDRhWQidmj2djHllOmjFExVFbDGCw1jdqDw= github.com/alitto/pond/v2 v2.5.0/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE= +github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= +github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= +github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI= +github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8= +github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= +github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c= +github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/core/adapters/clock.go b/internal/core/adapters/clock.go deleted file mode 100644 index cf4794d..0000000 --- a/internal/core/adapters/clock.go +++ /dev/null @@ -1,7 +0,0 @@ -package adapters - -import "time" - -type RealTimeClock struct{} - -func (RealTimeClock) Now() time.Time { return time.Now() } diff --git a/internal/core/adapters/es.go b/internal/core/adapters/es.go index db240a4..deadcd0 100644 --- a/internal/core/adapters/es.go +++ b/internal/core/adapters/es.go @@ -2,6 +2,7 @@ package adapters import ( "context" + "github.com/morebec/go-misas/misas" "sync" ) @@ -10,7 +11,7 @@ type InMemoryEventStore struct { events []any } -func (i *InMemoryEventStore) Record(ctx context.Context, event any) error { +func (i *InMemoryEventStore) Record(ctx context.Context, event any) misas.Error { i.mu.Lock() defer i.mu.Unlock() i.events = append(i.events, event) diff --git a/internal/core/adapters/run_repo.go b/internal/core/adapters/run_repo.go index 6f39cf4..988aa7b 100644 --- a/internal/core/adapters/run_repo.go +++ b/internal/core/adapters/run_repo.go @@ -2,6 +2,7 @@ package adapters import ( "context" + "github.com/morebec/go-misas/misas" "github.com/morebec/smallflow/internal/core" ) @@ -9,11 +10,11 @@ type EventStoreRunRepository struct { EventStore *InMemoryEventStore } -func (r EventStoreRunRepository) Add(ctx context.Context, run *core.Run) error { +func (r EventStoreRunRepository) Add(ctx context.Context, run *core.Run) misas.Error { return r.Save(ctx, run) } -func (r EventStoreRunRepository) Save(ctx context.Context, run *core.Run) error { +func (r EventStoreRunRepository) Save(ctx context.Context, run *core.Run) misas.Error { for _, event := range run.UncommittedEvents() { if err := r.EventStore.Record(ctx, event); err != nil { return err @@ -24,7 +25,7 @@ func (r EventStoreRunRepository) Save(ctx context.Context, run *core.Run) error return nil } -func (r EventStoreRunRepository) FindByID(_ context.Context, workflowID string, runID string) (*core.Run, error) { +func (r EventStoreRunRepository) FindByID(_ context.Context, workflowID string, runID string) (*core.Run, misas.Error) { var run *core.Run for _, event := range r.EventStore.events { switch ev := event.(type) { diff --git a/internal/core/adapters/workflow_repo.go b/internal/core/adapters/workflow_repo.go index a4711df..905a9bc 100644 --- a/internal/core/adapters/workflow_repo.go +++ b/internal/core/adapters/workflow_repo.go @@ -3,6 +3,7 @@ package adapters import ( "context" "fmt" + "github.com/morebec/go-misas/misas" "github.com/morebec/smallflow/internal/core" ) @@ -10,7 +11,7 @@ type EventStoreWorkflowRepository struct { EventStore *InMemoryEventStore } -func (e EventStoreWorkflowRepository) FindByID(_ context.Context, workflowID string) (*core.Workflow, error) { +func (e EventStoreWorkflowRepository) FindByID(_ context.Context, workflowID string) (*core.Workflow, misas.Error) { events := e.EventStore.Events() wf := core.NewWorkflow(core.WorkflowDefinition{ ID: core.WorkflowID(workflowID), @@ -51,7 +52,7 @@ func (e EventStoreWorkflowRepository) FindByID(_ context.Context, workflowID str return wf, nil } -func (e EventStoreWorkflowRepository) Save(ctx context.Context, wf *core.Workflow) error { +func (e EventStoreWorkflowRepository) Save(ctx context.Context, wf *core.Workflow) misas.Error { for _, event := range wf.UncommittedEvents() { if err := e.EventStore.Record(ctx, event); err != nil { return err diff --git a/internal/core/api.go b/internal/core/api.go index 23bd032..7e725a4 100644 --- a/internal/core/api.go +++ b/internal/core/api.go @@ -1,53 +1,43 @@ package core import ( - "context" "fmt" + "github.com/morebec/go-misas/misas" + "github.com/morebec/go-misas/muuid" + "github.com/morebec/go-misas/mx" "time" ) -type API struct { - EnableWorkflowCommandHandler EnableWorkflowCommandHandler - DisableWorkflowCommandHandler DisableWorkflowCommandHandler - TriggerWorkflowCommandHandler TriggerWorkflowCommandHandler - RunWorkflowCommandHandler RunWorkflowCommandHandler -} - -func NewAPI(clock Clock, workflowRepo WorkflowRepository, repo RunRepository) *API { - return &API{ - EnableWorkflowCommandHandler: EnableWorkflowCommandHandler{ +func NewSubsystem( + clock misas.Clock, + workflowRepo WorkflowRepository, + runRepository RunRepository, + uidg muuid.UUIDGenerator, +) misas.BusinessSubsystem { + return mx.NewBusinessSubsystemAssembler(). + WithCommandHandler(EnableWorkflowCommandTypeName, mx.NewTypedCommandHandler(EnableWorkflowCommandHandler{ + WorkflowRepository: workflowRepo, Clock: clock, + })). + WithCommandHandler(DisableWorkflowCommandTypeName, mx.NewTypedCommandHandler(DisableWorkflowCommandHandler{ WorkflowRepository: workflowRepo, - }, - DisableWorkflowCommandHandler: DisableWorkflowCommandHandler{ Clock: clock, + })). + WithCommandHandler(TriggerWorkflowCommandTypeName, mx.NewTypedCommandHandler(TriggerWorkflowCommandHandler{ WorkflowRepository: workflowRepo, - }, - TriggerWorkflowCommandHandler: TriggerWorkflowCommandHandler{ + UUIDGenerator: uidg, Clock: clock, + })). + WithCommandHandler(RunWorkflowCommandTypeName, mx.NewTypedCommandHandler(RunWorkflowCommandHandler{ WorkflowRepository: workflowRepo, - }, - RunWorkflowCommandHandler: RunWorkflowCommandHandler{ + RunRepository: runRepository, Clock: clock, + })). + WithCommandHandler(ResumeWorkflowRunCommandTypeName, mx.NewTypedCommandHandler(ResumeWorkflowRunCommandHandler{ WorkflowRepository: workflowRepo, - RunRepository: repo, - }, - } -} - -func (api API) HandleCommand(ctx context.Context, cmd any) error { - switch c := cmd.(type) { - case EnableWorkflowCommand: - return api.EnableWorkflowCommandHandler.Handle(ctx, c) - case DisableWorkflowCommand: - return api.DisableWorkflowCommandHandler.Handle(ctx, c) - case TriggerWorkflowCommand: - return api.TriggerWorkflowCommandHandler.Handle(ctx, c) - case RunWorkflowCommand: - return api.RunWorkflowCommandHandler.Handle(ctx, c) - default: - return fmt.Errorf("unknown command type: %T", cmd) - } + RunRepository: runRepository, + })). + Assemble() } // TriggerWorkflowCommand represents a command to trigger a new run of a @@ -69,12 +59,20 @@ type TriggerWorkflowCommand struct { RunID string } +const TriggerWorkflowCommandTypeName = "TriggerWorkflowCommand" + +func (TriggerWorkflowCommand) TypeName() misas.CommandTypeName { return TriggerWorkflowCommandTypeName } + type WorkflowTriggeredEvent struct { WorkflowID string RunID string TriggeredAt time.Time } +const WorkflowTriggeredEventTypeName = "WorkflowTriggeredEvent" + +func (WorkflowTriggeredEvent) TypeName() misas.EventTypeName { return WorkflowTriggeredEventTypeName } + // EnableWorkflowCommand represents a command to enable a workflow. Enabling a // workflow allows new runs to be triggered. If the workflow does not exist, this // command will fail. @@ -82,6 +80,10 @@ type EnableWorkflowCommand struct { WorkflowID string } +const EnableWorkflowCommandTypeName = "EnableWorkflowCommand" + +func (EnableWorkflowCommand) TypeName() misas.CommandTypeName { return EnableWorkflowCommandTypeName } + // WorkflowEnabledEvent is emitted when a workflow is successfully enabled. type WorkflowEnabledEvent struct { @@ -89,6 +91,10 @@ type WorkflowEnabledEvent struct { EnabledAt time.Time } +const WorkflowEnabledEventTypeName = "WorkflowEnabledEvent" + +func (WorkflowEnabledEvent) TypeName() misas.EventTypeName { return WorkflowEnabledEventTypeName } + // DisableWorkflowCommand represents a command to disable a workflow. Disabling a // workflow prevents new runs from being triggered, but does not affect currently // active runs. @@ -96,18 +102,30 @@ type DisableWorkflowCommand struct { WorkflowID string } +const DisableWorkflowCommandTypeName = "DisableWorkflowCommand" + +func (DisableWorkflowCommand) TypeName() misas.CommandTypeName { return DisableWorkflowCommandTypeName } + type WorkflowDisabledEvent struct { WorkflowID string DisabledAt time.Time ActiveRuns int } +const WorkflowDisabledEventTypeName = "WorkflowDisabledEvent" + +func (WorkflowDisabledEvent) TypeName() misas.EventTypeName { return WorkflowDisabledEventTypeName } + type WorkflowStartedEvent struct { WorkflowID string RunID string StartedAt time.Time } +const WorkflowStartedEventTypeName = "WorkflowStartedEvent" + +func (WorkflowStartedEvent) TypeName() misas.EventTypeName { return WorkflowStartedEventTypeName } + type WorkflowEndedEvent struct { WorkflowID string RunID string @@ -117,6 +135,10 @@ type WorkflowEndedEvent struct { Status string } +const WorkflowEndedEventTypeName = "WorkflowEndedEvent" + +func (WorkflowEndedEvent) TypeName() misas.EventTypeName { return WorkflowEndedEventTypeName } + type StepStartedEvent struct { WorkflowID string RunID string @@ -126,6 +148,10 @@ type StepStartedEvent struct { IgnoreErrors bool } +const StepStartedEventTypeName = "StepStartedEvent" + +func (StepStartedEvent) TypeName() misas.EventTypeName { return StepStartedEventTypeName } + type StepEndedEvent struct { WorkflowID string RunID string @@ -136,6 +162,10 @@ type StepEndedEvent struct { Status string } +const StepEndedEventTypeName = "StepEndedEvent" + +func (StepEndedEvent) TypeName() misas.EventTypeName { return StepEndedEventTypeName } + type WorkflowError struct { Kind string // e.g. "user", "system", "internal" Code string // e.g. "timeout", "network_error", "invalid_input" @@ -164,6 +194,21 @@ type RunWorkflowCommand struct { RunID string } +const RunWorkflowCommandTypeName = "RunWorkflowCommand" + +func (RunWorkflowCommand) TypeName() misas.CommandTypeName { return RunWorkflowCommandTypeName } + +type ResumeWorkflowRunCommand struct { + WorkflowID string + RunID string +} + +const ResumeWorkflowRunCommandTypeName = "ResumeWorkflowRunCommand" + +func (ResumeWorkflowRunCommand) TypeName() misas.CommandTypeName { + return ResumeWorkflowRunCommandTypeName +} + type WorkflowRunReport struct { WorkflowID string RunID string @@ -172,3 +217,11 @@ type WorkflowRunReport struct { Errors map[string]*WorkflowError Status string } + +const ErrorCodeWorkflowNotFound misas.ErrorCode = "workflow_not_found" + +var ErrWorkflowNotFound = mx.ErrNotFound.WithCode(ErrorCodeWorkflowNotFound).WithMessage("workflow not found") + +const ErrorCodeRunNotFound misas.ErrorCode = "workflow_run_not_found" + +var ErrWorkflowRunNotFound = mx.ErrNotFound.WithCode(ErrorCodeRunNotFound).WithMessage("workflow run not found") diff --git a/internal/core/run.go b/internal/core/run.go index 682d9cd..dbc2144 100644 --- a/internal/core/run.go +++ b/internal/core/run.go @@ -2,6 +2,8 @@ package core import ( "fmt" + "github.com/morebec/go-misas/misas" + "github.com/morebec/go-misas/mx" "time" ) @@ -37,19 +39,19 @@ func StartRun(workflowID WorkflowID, id RunID, startedAt time.Time) *Run { return run } -func (r *Run) StartStep(stepID StepID, id ActionID, ignoreErrors bool, currentTime time.Time) error { +func (r *Run) StartStep(stepID StepID, id ActionID, ignoreErrors bool, currentTime time.Time) misas.Error { if r.CurrentStepID == stepID { // already running, idempotent return nil } if r.CurrentStepID != "" { - return fmt.Errorf( - "workflow error: %s: cannot start step %s, step %s is already running", + return mx.ErrConflict.WithMessage(fmt.Sprintf( + "workflow error: %s: cannot start step %s: step %s is currently running", r.WorkflowID, stepID, r.CurrentStepID, - ) + )) } r.record(StepStartedEvent{ @@ -64,7 +66,7 @@ func (r *Run) StartStep(stepID StepID, id ActionID, ignoreErrors bool, currentTi return nil } -func (r *Run) EndStep(stepID StepID, err *WorkflowError, currentTime time.Time) error { +func (r *Run) EndStep(stepID StepID, err *WorkflowError, currentTime time.Time) misas.Error { step := r.Steps[stepID] if step.EndedAt != nil { // already ended, idempotent @@ -72,12 +74,12 @@ func (r *Run) EndStep(stepID StepID, err *WorkflowError, currentTime time.Time) } if r.CurrentStepID != stepID { - return fmt.Errorf( - "workflow error: %s: cannot end step %s, step %s is currently running", + return mx.ErrConflict.WithMessage(fmt.Sprintf( + "workflow error: %s: cannot start step %s: step %s is currently running", r.WorkflowID, stepID, r.CurrentStepID, - ) + )) } status := StepStatusSucceeded diff --git a/internal/core/runner.go b/internal/core/runner.go new file mode 100644 index 0000000..2b04c25 --- /dev/null +++ b/internal/core/runner.go @@ -0,0 +1,128 @@ +package core + +import ( + "context" + "fmt" + "github.com/morebec/go-misas/misas" + "github.com/morebec/go-misas/mx" +) + +type WorkflowRunner struct { + RunRepository RunRepository + WorkflowRepository WorkflowRepository + Clock misas.Clock +} + +func (r WorkflowRunner) Run(ctx context.Context, wf *Workflow, rID RunID) (WorkflowRunReport, misas.Error) { + workflowID := wf.ID() + run, err := r.RunRepository.FindByID(ctx, string(workflowID), string(rID)) + if err != nil { + return WorkflowRunReport{}, err + } + if run != nil { + // Run already exists, nothing to do. + return WorkflowRunReport{}, nil + } + + run = StartRun(workflowID, rID, r.Clock.Now()) + if err := r.RunRepository.Add(ctx, run); err != nil { + return WorkflowRunReport{}, err + } + + if err := r.executeRun(ctx, run, wf.Definition().Steps); err != nil { + return WorkflowRunReport{}, err + } + + return newWorkflowRunReport(run), nil +} + +func (r WorkflowRunner) ResumeFromStep(ctx context.Context, wf *Workflow, rID RunID) (WorkflowRunReport, misas.Error) { + workflowID := wf.ID() + run, err := r.RunRepository.FindByID(ctx, string(workflowID), string(rID)) + if err != nil { + return WorkflowRunReport{}, err + } + if run == nil { + // TODO: better error message. + return WorkflowRunReport{}, mx.ErrNotFound.WithMessage(fmt.Sprintf("workflow run not found: %s", rID)) + } + + steps := r.remainingStepsFrom(wf.Definition().Steps, run.CurrentStepID) + if err := r.executeRun(ctx, run, steps); err != nil { + return WorkflowRunReport{}, err + } + + return newWorkflowRunReport(run), nil +} + +func (r WorkflowRunner) runSteps(ctx context.Context, run *Run, steps []StepDefinition) misas.Error { + // If a start step ID is provided, set from to false until we encounter it. + for _, step := range steps { + if err := r.runStep(ctx, run, step); err != nil { + return err + } + } + + return nil +} + +func (r WorkflowRunner) runStep(ctx context.Context, run *Run, step StepDefinition) misas.Error { + if err := run.StartStep(step.ID, step.Action.ID(), step.IgnoreError, r.Clock.Now()); err != nil { + return err + } + if err := r.RunRepository.Save(ctx, run); err != nil { + return err + } + + workflowErr := r.runStepAction(ctx, step) + + if err := run.EndStep(step.ID, workflowErr, r.Clock.Now()); err != nil { + return err + } + if err := r.RunRepository.Save(ctx, run); err != nil { + return err + } + + return nil +} + +func (r WorkflowRunner) runStepAction(ctx context.Context, step StepDefinition) *WorkflowError { + return step.Action.Run(ctx) +} + +func (r WorkflowRunner) executeRun(ctx context.Context, run *Run, steps []StepDefinition) misas.Error { + err := r.runSteps(ctx, run, steps) + + run.End(r.Clock.Now()) + if repoErr := r.RunRepository.Save(ctx, run); repoErr != nil { + err = misas.ErrorGroup{err, repoErr} + } + + return err +} + +func (r WorkflowRunner) remainingStepsFrom(steps []StepDefinition, start StepID) []StepDefinition { + var remaining []StepDefinition + include := false + for _, step := range steps { + if !include && step.ID == start { + include = true + } + if include { + remaining = append(remaining, step) + } + } + + return remaining +} + +func newWorkflowRunReport(run *Run) WorkflowRunReport { + return WorkflowRunReport{ + WorkflowID: string(run.WorkflowID), + RunID: string(run.ID), + StartedAt: run.StartedAt, + EndedAt: *run.EndedAt, + Errors: run.Errors(), + Status: string(run.Status), + } +} diff --git a/internal/core/spi.go b/internal/core/spi.go index 927ac7a..2dc0a2c 100644 --- a/internal/core/spi.go +++ b/internal/core/spi.go @@ -2,18 +2,16 @@ package core import ( "context" - "time" + "github.com/morebec/go-misas/misas" ) type WorkflowRepository interface { - FindByID(ctx context.Context, workflowID string) (*Workflow, error) - Save(ctx context.Context, wf *Workflow) error + FindByID(ctx context.Context, workflowID string) (*Workflow, misas.Error) + Save(ctx context.Context, wf *Workflow) misas.Error } type RunRepository interface { - FindByID(ctx context.Context, workflowID string, runID string) (*Run, error) - Add(ctx context.Context, run *Run) error - Save(ctx context.Context, r *Run) error + FindByID(ctx context.Context, workflowID string, runID string) (*Run, misas.Error) + Add(ctx context.Context, run *Run) misas.Error + Save(ctx context.Context, r *Run) misas.Error } - -type Clock interface{ Now() time.Time } diff --git a/internal/core/util.go b/internal/core/util.go new file mode 100644 index 0000000..c51e376 --- /dev/null +++ b/internal/core/util.go @@ -0,0 +1,19 @@ +package core + +import "github.com/morebec/go-misas/misas" + +func NewWorkflowNotFoundError(workflowID any) misas.Error { + switch id := workflowID.(type) { + case WorkflowID: + return ErrWorkflowNotFound.WithAppendedMessage(string(id)) + case string: + return ErrWorkflowNotFound.WithAppendedMessage(id) + } + panic("invalid type for workflowID") +} + +func WorkflowNotFoundCommandResult(workflowID any) misas.CommandResult { + return misas.CommandResult{ + Error: NewWorkflowNotFoundError(workflowID), + } +} diff --git a/internal/core/workflow.go b/internal/core/workflow.go index a6bf5bd..dfdd6a6 100644 --- a/internal/core/workflow.go +++ b/internal/core/workflow.go @@ -2,6 +2,8 @@ package core import ( "fmt" + "github.com/morebec/go-misas/misas" + "github.com/morebec/go-misas/mx" "time" ) @@ -28,22 +30,23 @@ func NewWorkflow(d WorkflowDefinition, enabled bool) *Workflow { func (w *Workflow) ID() WorkflowID { return w.definition.ID } -func (w *Workflow) Trigger(id RunID, currentTime time.Time) error { +func (w *Workflow) Trigger(id RunID, currentTime time.Time) misas.Error { if _, exists := w.runIds[id]; exists { return nil // idempotent } + workflowID := w.ID() if !w.enabled { - return fmt.Errorf("workflow is not enabled: %s", w.ID()) + return mx.ErrConflict.WithMessage(fmt.Sprintf("workflow is not enabled: %s", workflowID)) } if w.definition.ConcurrencyLimit != ConcurrencyLimitNone && len(w.activeRuns) >= int(w.definition.ConcurrencyLimit) { - return fmt.Errorf("workflow concurrency limit reached: %s", w.ID()) + return mx.ErrConflict.WithMessage(fmt.Sprintf("workflow concurrency limit reached: %s", workflowID)) } w.record(WorkflowTriggeredEvent{ - WorkflowID: string(w.ID()), + WorkflowID: string(workflowID), RunID: string(id), TriggeredAt: currentTime, }) diff --git a/internal/core/workflow_disable.go b/internal/core/workflow_disable.go index 17f1778..aea69b7 100644 --- a/internal/core/workflow_disable.go +++ b/internal/core/workflow_disable.go @@ -2,24 +2,25 @@ package core import ( "context" - "fmt" + "github.com/morebec/go-misas/misas" + "github.com/morebec/go-misas/mx" ) type DisableWorkflowCommandHandler struct { - Clock Clock + Clock misas.Clock WorkflowRepository WorkflowRepository } -func (h DisableWorkflowCommandHandler) Handle(ctx context.Context, cmd DisableWorkflowCommand) error { +func (h DisableWorkflowCommandHandler) Handle(ctx context.Context, cmd DisableWorkflowCommand) misas.CommandResult { workflow, err := h.WorkflowRepository.FindByID(ctx, cmd.WorkflowID) if err != nil { - return err + return mx.CommandResultFromError(err) } if workflow == nil { - return fmt.Errorf("workflow not found: %s", cmd.WorkflowID) + return WorkflowNotFoundCommandResult(cmd.WorkflowID) } workflow.Disable(h.Clock.Now()) - return h.WorkflowRepository.Save(ctx, workflow) + return mx.CommandResultFromError(h.WorkflowRepository.Save(ctx, workflow)) } diff --git a/internal/core/workflow_enable.go b/internal/core/workflow_enable.go index 5ad45e5..6b9b9db 100644 --- a/internal/core/workflow_enable.go +++ b/internal/core/workflow_enable.go @@ -2,24 +2,25 @@ package core import ( "context" - "fmt" + "github.com/morebec/go-misas/misas" + "github.com/morebec/go-misas/mx" ) type EnableWorkflowCommandHandler struct { - Clock Clock + Clock misas.Clock WorkflowRepository WorkflowRepository } -func (h EnableWorkflowCommandHandler) Handle(ctx context.Context, cmd EnableWorkflowCommand) error { +func (h EnableWorkflowCommandHandler) Handle(ctx context.Context, cmd EnableWorkflowCommand) misas.CommandResult { workflow, err := h.WorkflowRepository.FindByID(ctx, cmd.WorkflowID) if err != nil { - return err + return mx.CommandResultFromError(err) } if workflow == nil { - return fmt.Errorf("workflow not found: %s", cmd.WorkflowID) + return WorkflowNotFoundCommandResult(cmd.WorkflowID) } workflow.Enable(h.Clock.Now()) - return h.WorkflowRepository.Save(ctx, workflow) + return mx.CommandResultFromError(h.WorkflowRepository.Save(ctx, workflow)) } diff --git a/internal/core/workflow_resume.go b/internal/core/workflow_resume.go new file mode 100644 index 0000000..0adc48b --- /dev/null +++ b/internal/core/workflow_resume.go @@ -0,0 +1,30 @@ +package core + +import ( + "context" + "github.com/morebec/go-misas/misas" + "github.com/morebec/go-misas/mx" +) + +type ResumeWorkflowRunCommandHandler struct { + WorkflowRepository WorkflowRepository + RunRepository RunRepository + Runner WorkflowRunner +} + +func (h ResumeWorkflowRunCommandHandler) Handle(ctx context.Context, cmd ResumeWorkflowRunCommand) misas.CommandResult { + wf, err := h.WorkflowRepository.FindByID(ctx, cmd.WorkflowID) + if err != nil { + return mx.CommandResultFromError(err) + } + if wf == nil { + return WorkflowNotFoundCommandResult(cmd.WorkflowID) + } + + report, err := h.Runner.ResumeFromStep(ctx, wf, RunID(cmd.RunID)) + if err != nil { + return mx.CommandResultFromError(err) + } + + return misas.CommandResult{Payload: report} +} diff --git a/internal/core/workflow_run.go b/internal/core/workflow_run.go index 9c7d90e..bd615b6 100644 --- a/internal/core/workflow_run.go +++ b/internal/core/workflow_run.go @@ -2,87 +2,36 @@ package core import ( "context" - "fmt" + "github.com/morebec/go-misas/misas" + "github.com/morebec/go-misas/mx" ) type RunWorkflowCommandHandler struct { - Clock Clock + Clock misas.Clock WorkflowRepository WorkflowRepository RunRepository RunRepository } -func (h RunWorkflowCommandHandler) Handle(ctx context.Context, cmd RunWorkflowCommand) error { - run, err := h.RunRepository.FindByID(ctx, cmd.WorkflowID, cmd.RunID) - if err != nil { - return err - } - if run != nil { - // Run already exists, nothing to do. - return nil - } - +func (h RunWorkflowCommandHandler) Handle(ctx context.Context, cmd RunWorkflowCommand) misas.CommandResult { wf, err := h.WorkflowRepository.FindByID(ctx, cmd.WorkflowID) if err != nil { - return err + return mx.CommandResultFromError(err) } if wf == nil { - return fmt.Errorf("workflow not found: %s", cmd.WorkflowID) - } - - run = StartRun(WorkflowID(cmd.WorkflowID), RunID(cmd.RunID), h.Clock.Now()) - if err := h.RunRepository.Add(ctx, run); err != nil { - return err + return WorkflowNotFoundCommandResult(cmd.WorkflowID) } - for _, step := range wf.Definition().Steps { - if err = h.runStep(ctx, run, step); err != nil { - break - } - } - - run.End(h.Clock.Now()) - if err := h.RunRepository.Save(ctx, run); err != nil { - return err + runner := WorkflowRunner{ + RunRepository: h.RunRepository, + WorkflowRepository: h.WorkflowRepository, + Clock: h.Clock, } + report, err := runner.Run(ctx, wf, RunID(cmd.RunID)) if err != nil { - return fmt.Errorf("workflow run failed: %s: %w", run.ID, err) + return mx.CommandResultFromError(err) } - report := WorkflowRunReport{ - WorkflowID: string(run.WorkflowID), - RunID: string(run.ID), - StartedAt: run.StartedAt, - EndedAt: *run.EndedAt, - Errors: run.Errors(), - Status: string(run.Status), - } - - fmt.Printf("Workflow run completed: %+v \n", report) - - return nil -} - -func (h RunWorkflowCommandHandler) runStep(ctx context.Context, run *Run, step StepDefinition) error { - if err := run.StartStep(step.ID, step.Action.ID(), step.IgnoreError, h.Clock.Now()); err != nil { - return err - } - if err := h.RunRepository.Save(ctx, run); err != nil { - return err - } - - workflowErr := h.runStepAction(ctx, step) - - if err := run.EndStep(step.ID, workflowErr, h.Clock.Now()); err != nil { - return err - } - if err := h.RunRepository.Save(ctx, run); err != nil { - return err - } - - return nil -} + return misas.CommandResult{Payload: report} -func (h RunWorkflowCommandHandler) runStepAction(ctx context.Context, step StepDefinition) *WorkflowError { - return step.Action.Run(ctx) } diff --git a/internal/core/workflow_trigger.go b/internal/core/workflow_trigger.go index 573ab87..556401e 100644 --- a/internal/core/workflow_trigger.go +++ b/internal/core/workflow_trigger.go @@ -2,36 +2,33 @@ package core import ( "context" - "fmt" + "github.com/morebec/go-misas/misas" + "github.com/morebec/go-misas/muuid" + "github.com/morebec/go-misas/mx" ) type TriggerWorkflowCommandHandler struct { WorkflowRepository WorkflowRepository - Clock Clock + UUIDGenerator muuid.UUIDGenerator + Clock misas.Clock } -func (h TriggerWorkflowCommandHandler) Handle(ctx context.Context, cmd TriggerWorkflowCommand) error { +func (h TriggerWorkflowCommandHandler) Handle(ctx context.Context, cmd TriggerWorkflowCommand) misas.CommandResult { wf, err := h.WorkflowRepository.FindByID(ctx, cmd.WorkflowID) if err != nil { - return err + return mx.CommandResultFromError(err) } if wf == nil { - return fmt.Errorf("workflow not found: %s", cmd.WorkflowID) - } - - if cmd.WorkflowID == "" { - return fmt.Errorf("workflow ID is required") + return WorkflowNotFoundCommandResult(cmd.WorkflowID) } if cmd.RunID == "" { - cmd.RunID = generateRunID() + cmd.RunID = h.UUIDGenerator.Generate().String() } if err := wf.Trigger(RunID(cmd.RunID), h.Clock.Now()); err != nil { - return err + return mx.CommandResultFromError(err) } - return h.WorkflowRepository.Save(ctx, wf) + return mx.CommandResultFromError(h.WorkflowRepository.Save(ctx, wf)) } - -func generateRunID() string { return "some-generated-run-id" } diff --git a/internal/orchestrator/adapters/lease.go b/internal/orchestrator/adapters/lease.go new file mode 100644 index 0000000..f2957f1 --- /dev/null +++ b/internal/orchestrator/adapters/lease.go @@ -0,0 +1,37 @@ +package adapters + +import ( + "context" + "github.com/morebec/smallflow/internal/orchestrator" +) + +type InMemoryWorkflowLeaseRepository struct { + leases map[string]orchestrator.WorkflowLease +} + +func NewInMemoryWorkflowLeaseRepository() *InMemoryWorkflowLeaseRepository { + return &InMemoryWorkflowLeaseRepository{leases: make(map[string]orchestrator.WorkflowLease)} +} + +func (r InMemoryWorkflowLeaseRepository) Add(_ context.Context, lease orchestrator.WorkflowLease) error { + r.leases[lease.WorkflowID+lease.RunID] = lease + return nil +} + +func (r InMemoryWorkflowLeaseRepository) Update(_ context.Context, lease orchestrator.WorkflowLease) error { + r.leases[lease.WorkflowID+lease.RunID] = lease + return nil +} + +func (r InMemoryWorkflowLeaseRepository) Remove(_ context.Context, workflowID string, runID string) error { + delete(r.leases, workflowID+runID) + return nil +} + +func (r InMemoryWorkflowLeaseRepository) FindByWorkflowRunID(ctx context.Context, workflowID string, runID string) (*orchestrator.WorkflowLease, error) { + lease, ok := r.leases[workflowID+runID] + if !ok { + return nil, nil + } + return &lease, nil +} diff --git a/internal/orchestrator/lease.go b/internal/orchestrator/lease.go new file mode 100644 index 0000000..710a230 --- /dev/null +++ b/internal/orchestrator/lease.go @@ -0,0 +1,72 @@ +package orchestrator + +import ( + "context" + "github.com/morebec/go-misas/misas" + "time" +) + +const defaultLeaseDuration = 5 * time.Minute + +type WorkflowLease struct { + WorkflowID string + RunID string + expiresAt time.Time +} + +func (l WorkflowLease) IsExpired(currentTime time.Time) bool { return currentTime.After(l.expiresAt) } + +type WorkflowLeaseRepository interface { + Add(context.Context, WorkflowLease) error + Update(context.Context, WorkflowLease) error + Remove(ctx context.Context, workflowID string, runID string) error + FindByWorkflowRunID(ctx context.Context, workflowID string, runID string) (*WorkflowLease, error) +} + +type WorkflowLeaseManager struct { + Clock misas.Clock + Repository WorkflowLeaseRepository +} + +func (l WorkflowLeaseManager) TryAcquire(ctx context.Context, workflowID string, runID string) (bool, error) { + lease, err := l.Repository.FindByWorkflowRunID(ctx, workflowID, runID) + if err != nil { + return false, err + } + + if lease != nil && !lease.IsExpired(l.Clock.Now()) { + // Lease is still valid + return false, nil + } + + wl := WorkflowLease{ + WorkflowID: workflowID, + RunID: runID, + expiresAt: l.Clock.Now().Add(defaultLeaseDuration), + } + if err := l.Repository.Add(ctx, wl); err != nil { + return false, err + } + + return true, nil +} + +func (l WorkflowLeaseManager) Release(ctx context.Context, workflowID string, runID string) error { + if err := l.Repository.Remove(ctx, workflowID, runID); err != nil { + return err + } + + return nil +} + +func (l WorkflowLeaseManager) RenewLease(ctx context.Context, workflowID string, runID string) error { + if err := l.Repository.Update(ctx, WorkflowLease{ + WorkflowID: workflowID, + RunID: runID, + expiresAt: l.Clock.Now().Add(defaultLeaseDuration), + }); err != nil { + return err + } + + return nil +} diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 46587a1..9f50eb5 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -2,15 +2,36 @@ package orchestrator import ( "context" + "fmt" + "github.com/alitto/pond/v2" + "github.com/morebec/go-misas/misas" "github.com/morebec/smallflow/internal/core" ) type WorkflowOrchestrator struct { - Clock core.Clock - API *core.API + Clock misas.Clock + API misas.BusinessAPI + LeaseManager WorkflowLeaseManager + Pool pond.Pool + + ctx context.Context + cancelCtx context.CancelFunc +} + +func (m *WorkflowOrchestrator) Start() { + m.ctx, m.cancelCtx = context.WithCancel(context.Background()) +} + +func (m *WorkflowOrchestrator) Stop() { m.cancelCtx() } + +func (m *WorkflowOrchestrator) IsRunning() bool { + if m.ctx == nil { + return false + } + return m.ctx.Err() == nil } -func (m WorkflowOrchestrator) HandleEvent(ctx context.Context, event any) error { +func (m *WorkflowOrchestrator) HandleEvent(ctx context.Context, event any) error { switch e := event.(type) { case core.WorkflowTriggeredEvent: return m.runWorkflow(ctx, e) @@ -19,9 +40,17 @@ func (m WorkflowOrchestrator) HandleEvent(ctx context.Context, event any) error return nil } -func (m WorkflowOrchestrator) runWorkflow(ctx context.Context, e core.WorkflowTriggeredEvent) error { - return m.API.HandleCommand(ctx, core.RunWorkflowCommand{ - WorkflowID: e.WorkflowID, - RunID: e.RunID, +func (m *WorkflowOrchestrator) runWorkflow(_ context.Context, e core.WorkflowTriggeredEvent) error { + pond.Submit(func() { + runner := Worker{ + Clock: m.Clock, + API: m.API, + LeaseManager: m.LeaseManager, + InstanceID: "instance-1", // TODO: generate unique instance ID + } + if err := runner.Run(context.Background(), e.WorkflowID, e.RunID); err != nil { + fmt.Println("workflow runner error:", err) + } }) + return nil } diff --git a/internal/orchestrator/runner.go b/internal/orchestrator/runner.go new file mode 100644 index 0000000..aebde9f --- /dev/null +++ b/internal/orchestrator/runner.go @@ -0,0 +1,89 @@ +package orchestrator + +import ( + "context" + "fmt" + "github.com/morebec/go-misas/misas" + "github.com/morebec/smallflow/internal/core" + "time" +) + +const defaultLeaseHeartbeatDuration = time.Second * 30 + +type Worker struct { + Clock misas.Clock + API misas.BusinessAPI + LeaseManager WorkflowLeaseManager + InstanceID string +} + +func (r Worker) acquireLease(ctx context.Context, workflowID, runID string) (context.Context, context.CancelFunc, error) { + acquired, err := r.LeaseManager.TryAcquire(ctx, workflowID, runID) + if err != nil { + return nil, nil, err + } + + if !acquired { + // Could not acquire lease, another runner instance is likely handling this workflow run. + fmt.Printf("Could not acquire lease for workflow run: {workflowID: %s, runID: %s}\n", workflowID, runID) + return nil, nil, nil + } + fmt.Printf("Lease acquired for workflow run: {workflowID: %s, runID: %s}\n", workflowID, runID) + + ctx, cancel := context.WithCancel(ctx) + + ticker := time.NewTicker(defaultLeaseHeartbeatDuration) + go func() { + for { + select { + case <-ticker.C: + if err := r.LeaseManager.RenewLease(ctx, workflowID, runID); err != nil { + fmt.Printf( + "Failed to renew lease for workflow run: {workflowID: %s, runID: %s}: %s\n", + workflowID, + runID, + err, + ) + } + case <-ctx.Done(): + return + } + } + }() + + stopHeartbeat := func() { + ticker.Stop() + cancel() + // Release the lease when stopping the heartbeat + if err := r.LeaseManager.Release(ctx, workflowID, runID); err != nil { + fmt.Printf( + "Failed to release lease for workflow run: {workflowID: %s, runID: %s}: %s\n", + workflowID, + runID, + err, + ) + } + fmt.Printf("Lease released for workflow run: {workflowID: %s, runID: %s}\n", workflowID, runID) + } + + return ctx, stopHeartbeat, nil +} + +func (r Worker) Run(ctx context.Context, workflowID, runID string) error { + ctx, releaseLease, err := r.acquireLease(ctx, workflowID, runID) + if err != nil { + return err + } + if releaseLease == nil { + // Lease not acquired, another runner is handling this workflow run. + return nil + } + defer releaseLease() + + result := r.API.HandleCommand(ctx, core.RunWorkflowCommand{ + WorkflowID: workflowID, + RunID: runID, + }) + + return result.Error +} From 7bf9e21f8d17bf862e4c7d3f9080d1f896724a94 Mon Sep 17 00:00:00 2001 From: = Date: Sun, 19 Oct 2025 04:46:13 -0400 Subject: [PATCH 3/6] feat: Add workflows --- cmd/smallflow/main.go | 116 ++++--- compose.debug.yaml | 8 - compose.yaml | 26 +- go.mod | 3 +- go.sum | 4 + internal/core/adapters/es.go | 27 -- internal/core/adapters/run_repo.go | 45 --- internal/core/adapters/workflow_repo.go | 65 ---- internal/orchestrator/adapters/lease.go | 37 --- .../orchestrator/adapters/lease_inmemory.go | 47 +++ .../orchestrator/adapters/lease_postgres.go | 86 +++++ internal/orchestrator/lease.go | 2 +- internal/orchestrator/orchestrator.go | 74 +++-- .../orchestrator/{runner.go => worker.go} | 7 +- internal/{core => workflowmgmt}/action.go | 2 +- internal/workflowmgmt/adapters/run_repo.go | 100 ++++++ .../workflowmgmt/adapters/workflow_repo.go | 94 ++++++ internal/{core => workflowmgmt}/api.go | 2 +- internal/{core => workflowmgmt}/run.go | 13 +- internal/{core => workflowmgmt}/runner.go | 2 +- internal/{core => workflowmgmt}/spi.go | 2 +- internal/{core => workflowmgmt}/util.go | 2 +- internal/{core => workflowmgmt}/workflow.go | 12 +- .../workflow_definition.go | 2 +- .../workflow_disable.go | 2 +- .../{core => workflowmgmt}/workflow_enable.go | 2 +- .../{core => workflowmgmt}/workflow_resume.go | 2 +- .../{core => workflowmgmt}/workflow_run.go | 2 +- .../workflow_trigger.go | 2 +- specs/business/workflowmgmt.def.hcl | 295 ++++++++++++++++++ specs/smallflow.def.hcl | 31 ++ 31 files changed, 835 insertions(+), 279 deletions(-) delete mode 100644 compose.debug.yaml delete mode 100644 internal/core/adapters/es.go delete mode 100644 internal/core/adapters/run_repo.go delete mode 100644 internal/core/adapters/workflow_repo.go delete mode 100644 internal/orchestrator/adapters/lease.go create mode 100644 internal/orchestrator/adapters/lease_inmemory.go create mode 100644 internal/orchestrator/adapters/lease_postgres.go rename internal/orchestrator/{runner.go => worker.go} (87%) rename internal/{core => workflowmgmt}/action.go (95%) create mode 100644 internal/workflowmgmt/adapters/run_repo.go create mode 100644 internal/workflowmgmt/adapters/workflow_repo.go rename internal/{core => workflowmgmt}/api.go (99%) rename internal/{core => workflowmgmt}/run.go (94%) rename internal/{core => workflowmgmt}/runner.go (99%) rename internal/{core => workflowmgmt}/spi.go (95%) rename internal/{core => workflowmgmt}/util.go (95%) rename internal/{core => workflowmgmt}/workflow.go (89%) rename internal/{core => workflowmgmt}/workflow_definition.go (93%) rename internal/{core => workflowmgmt}/workflow_disable.go (96%) rename internal/{core => workflowmgmt}/workflow_enable.go (96%) rename internal/{core => workflowmgmt}/workflow_resume.go (97%) rename internal/{core => workflowmgmt}/workflow_run.go (97%) rename internal/{core => workflowmgmt}/workflow_trigger.go (97%) create mode 100644 specs/business/workflowmgmt.def.hcl create mode 100644 specs/smallflow.def.hcl diff --git a/cmd/smallflow/main.go b/cmd/smallflow/main.go index bb70f74..5a9ad88 100644 --- a/cmd/smallflow/main.go +++ b/cmd/smallflow/main.go @@ -3,83 +3,109 @@ package main import ( "context" "fmt" - "github.com/alitto/pond/v2" + "github.com/morebec/go-misas/misas" + "github.com/morebec/go-misas/mpostgres" "github.com/morebec/go-misas/muuid" "github.com/morebec/go-misas/mx" - "github.com/morebec/smallflow/internal/core" - "github.com/morebec/smallflow/internal/core/adapters" "github.com/morebec/smallflow/internal/orchestrator" adapters2 "github.com/morebec/smallflow/internal/orchestrator/adapters" + "github.com/morebec/smallflow/internal/workflowmgmt" + "github.com/morebec/smallflow/internal/workflowmgmt/adapters" "time" ) func main() { fmt.Println("Build, run, and observe workflows without the overhead!") - eventStore := &adapters.InMemoryEventStore{} clock := mx.NewRealTimeClock(time.UTC) + + dbConn, err := mpostgres.OpenConn("postgres://smallflow:smallflow@localhost:5432/postgres?sslmode=disable") + if err != nil { + panic(err) + } + + var eventStore misas.EventStore + eventStore, err = mpostgres.NewEventStore(clock, dbConn) + if err != nil { + panic(err) + } + + eventRegistry := mx.NewMessageRegistry[misas.EventTypeName, misas.Event]() + + eventRegistry.Register(workflowmgmt.WorkflowEnabledEventTypeName, workflowmgmt.WorkflowEnabledEvent{}) + eventRegistry.Register(workflowmgmt.WorkflowDisabledEventTypeName, workflowmgmt.WorkflowDisabledEvent{}) + eventRegistry.Register(workflowmgmt.WorkflowTriggeredEventTypeName, workflowmgmt.WorkflowTriggeredEvent{}) + eventRegistry.Register(workflowmgmt.WorkflowStartedEventTypeName, workflowmgmt.WorkflowStartedEvent{}) + eventRegistry.Register(workflowmgmt.WorkflowEndedEventTypeName, workflowmgmt.WorkflowEndedEvent{}) + eventRegistry.Register(workflowmgmt.StepStartedEventTypeName, workflowmgmt.StepStartedEvent{}) + eventRegistry.Register(workflowmgmt.StepEndedEventTypeName, workflowmgmt.StepEndedEvent{}) + + eventStore = mx.NewEventStoreDeserializerDecorator(eventStore, eventRegistry) + workflowRepo := &adapters.EventStoreWorkflowRepository{ - EventStore: eventStore, + EventStore: eventStore, + EventRegistry: eventRegistry, + UUIDGenerator: muuid.NewRandomUUIDGenerator(), } runRepo := &adapters.EventStoreRunRepository{ - EventStore: eventStore, + EventStore: eventStore, + EventRegistry: eventRegistry, + UUIDGenerator: muuid.NewRandomUUIDGenerator(), } - api := core.NewSubsystem(clock, workflowRepo, runRepo, muuid.NewRandomUUIDGenerator()).API - orch := &orchestrator.WorkflowOrchestrator{ - Clock: clock, - API: api, - LeaseManager: orchestrator.WorkflowLeaseManager{ - Clock: clock, - Repository: adapters2.NewInMemoryWorkflowLeaseRepository(), - }, - Pool: pond.NewPool(10), + api := workflowmgmt.NewSubsystem(clock, workflowRepo, runRepo, muuid.NewRandomUUIDGenerator()).API + workflowLeaseRepository, err := adapters2.NewPostgresWorkflowLeaseRepository(dbConn) + if err != nil { + panic(err) } - ctx := context.Background() - - fmt.Println("Enabling workflow...") - if result := api.HandleCommand(ctx, core.EnableWorkflowCommand{ - WorkflowID: "my-workflow", - }); result.Error != nil { - panic(result.Error) + leaseManager := orchestrator.WorkflowLeaseManager{ + Clock: clock, + Repository: workflowLeaseRepository, } - fmt.Println("Triggering workflow...") - if result := api.HandleCommand(ctx, core.TriggerWorkflowCommand{ - WorkflowID: "my-workflow", - RunID: muuid.NewRandomUUIDGenerator().Generate().String(), - }); result.Error != nil { - panic(result.Error) + checkpointStore, err := mpostgres.NewPostgreSQLCheckpointStore(dbConn) + if err != nil { + panic(err) } + orch := orchestrator.NewWorkflowOrchestrator( + clock, + api, + leaseManager, + muuid.NewRandomUUIDGenerator(), + eventStore, + checkpointStore, + ) + orch.Start() + defer orch.Stop() + + ctx := context.Background() - fmt.Println("Disabling workflow...") - if result := api.HandleCommand(ctx, core.DisableWorkflowCommand{ + fmt.Println("Enabling workflow...") + if result := api.HandleCommand(ctx, workflowmgmt.EnableWorkflowCommand{ WorkflowID: "my-workflow", }); result.Error != nil { panic(result.Error) } - fmt.Println("Dispatching events through the orchestrator...") - orch.Start() - defer orch.Stop() - - for _, event := range eventStore.Events() { - if err := orch.HandleEvent(ctx, event); err != nil { - panic(err) + for i := range 1 { + fmt.Printf("Triggering workflow #%d...\n", i+1) + if result := api.HandleCommand(ctx, workflowmgmt.TriggerWorkflowCommand{ + WorkflowID: "my-workflow", + RunID: muuid.NewRandomUUIDGenerator().Generate().String(), + }); result.Error != nil { + panic(result.Error) } } - for orch.IsRunning() { - select { - case <-time.After(15 * time.Second): - fmt.Println("Stopping orchestrator after 15 seconds...") - orch.Stop() - } + <-time.After(30 * time.Second) + fmt.Println("Current events in the event store:") + stream, err := eventStore.ReadFromStream(ctx, eventStore.GlobalStreamID(), misas.ReadFromEventStreamOptions{}.FromStart().Forward()) + if err != nil { + panic(err) } - fmt.Println("Current events in the event store:") - for i, event := range eventStore.Events() { + for i, event := range stream.Events { fmt.Printf("Event %d: %T → %+v\n", i, event, event) } } diff --git a/compose.debug.yaml b/compose.debug.yaml deleted file mode 100644 index e97b22b..0000000 --- a/compose.debug.yaml +++ /dev/null @@ -1,8 +0,0 @@ -services: - smallflow: - image: smallflow - build: - context: . - dockerfile: ./Dockerfile - ports: - - 3000:3000 diff --git a/compose.yaml b/compose.yaml index e97b22b..c297f2d 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,8 +1,22 @@ services: - smallflow: - image: smallflow - build: - context: . - dockerfile: ./Dockerfile +# smallflow: +# image: smallflow +# build: +# context: . +# dockerfile: ./Dockerfile +# ports: +# - 3000:3000 + + postgres: + image: postgres:18-alpine + environment: + POSTGRES_USER: smallflow + POSTGRES_PASSWORD: smallflow + POSTGRES_DB: smallflow ports: - - 3000:3000 + - "5432:5432" + volumes: + - postgres:/var/lib/postgresql/data + +volumes: + postgres: diff --git a/go.mod b/go.mod index 689f4c0..9c64181 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,12 @@ replace github.com/morebec/go-misas => ./../go-misas-back require ( github.com/alitto/pond/v2 v2.5.0 github.com/morebec/go-misas v0.0.0-00010101000000-000000000000 + github.com/samber/lo v1.49.1 ) require ( github.com/google/uuid v1.6.0 // indirect - github.com/samber/lo v1.49.1 // indirect + github.com/lib/pq v1.10.9 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect golang.org/x/text v0.24.0 // indirect diff --git a/go.sum b/go.sum index d4ccfad..7807215 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/alitto/pond/v2 v2.5.0 h1:vPzS5GnvSDRhWQidmj2djHllOmjFExVFbDGCw1jdqDw= github.com/alitto/pond/v2 v2.5.0/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE= github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= @@ -20,6 +22,8 @@ github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8Io github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= diff --git a/internal/core/adapters/es.go b/internal/core/adapters/es.go deleted file mode 100644 index deadcd0..0000000 --- a/internal/core/adapters/es.go +++ /dev/null @@ -1,27 +0,0 @@ -package adapters - -import ( - "context" - "github.com/morebec/go-misas/misas" - "sync" -) - -type InMemoryEventStore struct { - mu sync.Mutex - events []any -} - -func (i *InMemoryEventStore) Record(ctx context.Context, event any) misas.Error { - i.mu.Lock() - defer i.mu.Unlock() - i.events = append(i.events, event) - return nil -} - -func (i *InMemoryEventStore) Events() []any { - i.mu.Lock() - defer i.mu.Unlock() - eventsCopy := make([]any, len(i.events)) - copy(eventsCopy, i.events) - return eventsCopy -} diff --git a/internal/core/adapters/run_repo.go b/internal/core/adapters/run_repo.go deleted file mode 100644 index 988aa7b..0000000 --- a/internal/core/adapters/run_repo.go +++ /dev/null @@ -1,45 +0,0 @@ -package adapters - -import ( - "context" - "github.com/morebec/go-misas/misas" - "github.com/morebec/smallflow/internal/core" -) - -type EventStoreRunRepository struct { - EventStore *InMemoryEventStore -} - -func (r EventStoreRunRepository) Add(ctx context.Context, run *core.Run) misas.Error { - return r.Save(ctx, run) -} - -func (r EventStoreRunRepository) Save(ctx context.Context, run *core.Run) misas.Error { - for _, event := range run.UncommittedEvents() { - if err := r.EventStore.Record(ctx, event); err != nil { - return err - } - } - run.Commit() - - return nil -} - -func (r EventStoreRunRepository) FindByID(_ context.Context, workflowID string, runID string) (*core.Run, misas.Error) { - var run *core.Run - for _, event := range r.EventStore.events { - switch ev := event.(type) { - case core.WorkflowStartedEvent: - run = &core.Run{ - ID: core.RunID(runID), - WorkflowID: core.WorkflowID(workflowID), - } - if ev.RunID == runID && ev.WorkflowID == workflowID { - run.Apply([]any{ev}) - } - } - } - - return run, nil - -} diff --git a/internal/core/adapters/workflow_repo.go b/internal/core/adapters/workflow_repo.go deleted file mode 100644 index 905a9bc..0000000 --- a/internal/core/adapters/workflow_repo.go +++ /dev/null @@ -1,65 +0,0 @@ -package adapters - -import ( - "context" - "fmt" - "github.com/morebec/go-misas/misas" - "github.com/morebec/smallflow/internal/core" -) - -type EventStoreWorkflowRepository struct { - EventStore *InMemoryEventStore -} - -func (e EventStoreWorkflowRepository) FindByID(_ context.Context, workflowID string) (*core.Workflow, misas.Error) { - events := e.EventStore.Events() - wf := core.NewWorkflow(core.WorkflowDefinition{ - ID: core.WorkflowID(workflowID), - ConcurrencyLimit: core.ConcurrencyLimitNone, - Steps: []core.StepDefinition{ - { - ID: "step-1", - IgnoreError: false, - Action: core.NewActionFunc("my-action", func(ctx context.Context) *core.WorkflowError { - fmt.Println("Executing my-action") - return nil - }), - }, - { - ID: "step-2", - IgnoreError: false, - Action: core.NewActionFunc("my-action-2", func(ctx context.Context) *core.WorkflowError { - fmt.Println("Executing my-action 2") - return &core.WorkflowError{ - Kind: "internal", - Code: "not_implemented", - Message: "this action is not implemented", - Details: map[string]any{"action_id": "my-action-2"}, - } - //return nil - }), - }, - }, - }, false) - - for _, event := range events { - switch ev := event.(type) { - case core.WorkflowEnabledEvent, core.WorkflowDisabledEvent, core.WorkflowTriggeredEvent: - wf.Apply([]any{ev}) - } - } - - return wf, nil -} - -func (e EventStoreWorkflowRepository) Save(ctx context.Context, wf *core.Workflow) misas.Error { - for _, event := range wf.UncommittedEvents() { - if err := e.EventStore.Record(ctx, event); err != nil { - return err - } - } - - wf.Commit() - - return nil -} diff --git a/internal/orchestrator/adapters/lease.go b/internal/orchestrator/adapters/lease.go deleted file mode 100644 index f2957f1..0000000 --- a/internal/orchestrator/adapters/lease.go +++ /dev/null @@ -1,37 +0,0 @@ -package adapters - -import ( - "context" - "github.com/morebec/smallflow/internal/orchestrator" -) - -type InMemoryWorkflowLeaseRepository struct { - leases map[string]orchestrator.WorkflowLease -} - -func NewInMemoryWorkflowLeaseRepository() *InMemoryWorkflowLeaseRepository { - return &InMemoryWorkflowLeaseRepository{leases: make(map[string]orchestrator.WorkflowLease)} -} - -func (r InMemoryWorkflowLeaseRepository) Add(_ context.Context, lease orchestrator.WorkflowLease) error { - r.leases[lease.WorkflowID+lease.RunID] = lease - return nil -} - -func (r InMemoryWorkflowLeaseRepository) Update(_ context.Context, lease orchestrator.WorkflowLease) error { - r.leases[lease.WorkflowID+lease.RunID] = lease - return nil -} - -func (r InMemoryWorkflowLeaseRepository) Remove(_ context.Context, workflowID string, runID string) error { - delete(r.leases, workflowID+runID) - return nil -} - -func (r InMemoryWorkflowLeaseRepository) FindByWorkflowRunID(ctx context.Context, workflowID string, runID string) (*orchestrator.WorkflowLease, error) { - lease, ok := r.leases[workflowID+runID] - if !ok { - return nil, nil - } - return &lease, nil -} diff --git a/internal/orchestrator/adapters/lease_inmemory.go b/internal/orchestrator/adapters/lease_inmemory.go new file mode 100644 index 0000000..fc5649c --- /dev/null +++ b/internal/orchestrator/adapters/lease_inmemory.go @@ -0,0 +1,47 @@ +package adapters + +import ( + "context" + "github.com/morebec/smallflow/internal/orchestrator" + "sync" +) + +type InMemoryWorkflowLeaseRepository struct { + mu sync.Mutex + leases map[string]orchestrator.WorkflowLease +} + +func NewInMemoryWorkflowLeaseRepository() *InMemoryWorkflowLeaseRepository { + return &InMemoryWorkflowLeaseRepository{leases: make(map[string]orchestrator.WorkflowLease)} +} + +func (r *InMemoryWorkflowLeaseRepository) Add(_ context.Context, lease orchestrator.WorkflowLease) error { + r.mu.Lock() + defer r.mu.Unlock() + r.leases[lease.WorkflowID+lease.RunID] = lease + return nil +} + +func (r *InMemoryWorkflowLeaseRepository) Update(_ context.Context, lease orchestrator.WorkflowLease) error { + r.mu.Lock() + defer r.mu.Unlock() + r.leases[lease.WorkflowID+lease.RunID] = lease + return nil +} + +func (r *InMemoryWorkflowLeaseRepository) Remove(_ context.Context, workflowID string, runID string) error { + r.mu.Lock() + defer r.mu.Unlock() + delete(r.leases, workflowID+runID) + return nil +} + +func (r *InMemoryWorkflowLeaseRepository) FindByWorkflowRunID(_ context.Context, workflowID string, runID string) (*orchestrator.WorkflowLease, error) { + r.mu.Lock() + defer r.mu.Unlock() + lease, ok := r.leases[workflowID+runID] + if !ok { + return nil, nil + } + return &lease, nil +} diff --git a/internal/orchestrator/adapters/lease_postgres.go b/internal/orchestrator/adapters/lease_postgres.go new file mode 100644 index 0000000..d6a61f7 --- /dev/null +++ b/internal/orchestrator/adapters/lease_postgres.go @@ -0,0 +1,86 @@ +package adapters + +import ( + "context" + "github.com/morebec/go-misas/mpostgres" + "github.com/morebec/smallflow/internal/orchestrator" +) + +type PostgresWorkflowLeaseRepository struct { + conn mpostgres.DB + collection mpostgres.Collection +} + +func NewPostgresWorkflowLeaseRepository(conn mpostgres.DB) (PostgresWorkflowLeaseRepository, error) { + ctx := context.Background() + docStore, err := mpostgres.NewDocumentStore(ctx, conn) + if err != nil { + return PostgresWorkflowLeaseRepository{}, err + } + + collection, err := docStore.Collection("workflow_leases") + if err != nil { + return PostgresWorkflowLeaseRepository{}, err + } + if err := collection.Create(ctx); err != nil { + return PostgresWorkflowLeaseRepository{}, err + } + + return PostgresWorkflowLeaseRepository{conn: conn, collection: collection}, nil +} + +func (r PostgresWorkflowLeaseRepository) Add(ctx context.Context, lease orchestrator.WorkflowLease) error { + doc, err := mpostgres.NewDocument(r.workflowLeasID(lease.WorkflowID, lease.RunID), lease) + if err != nil { + return err + } + + if err := r.collection.Add(ctx, doc); err != nil { + return err + } + + return nil +} + +func (r PostgresWorkflowLeaseRepository) workflowLeasID(workflowID, runID string) string { + return workflowID + "/" + runID +} + +func (r PostgresWorkflowLeaseRepository) Update(ctx context.Context, lease orchestrator.WorkflowLease) error { + doc, err := mpostgres.NewDocument(r.workflowLeasID(lease.WorkflowID, lease.RunID), lease) + if err != nil { + return err + } + + if _, err := r.collection.Update(ctx, doc); err != nil { + return err + } + + return nil + +} + +func (r PostgresWorkflowLeaseRepository) Remove(ctx context.Context, workflowID string, runID string) error { + if _, err := r.collection.RemoveByID(ctx, r.workflowLeasID(workflowID, runID)); err != nil { + return err + } + + return nil +} + +func (r PostgresWorkflowLeaseRepository) FindByWorkflowRunID(ctx context.Context, workflowID string, runID string) (*orchestrator.WorkflowLease, error) { + doc, err := r.collection.FindByID(ctx, r.workflowLeasID(workflowID, runID)) + if err != nil { + return nil, err + } + if doc == nil { + return nil, nil + } + + var wl *orchestrator.WorkflowLease + if err := doc.Unmarshal(&wl); err != nil { + return nil, err + } + + return wl, nil +} diff --git a/internal/orchestrator/lease.go b/internal/orchestrator/lease.go index 710a230..8b096e7 100644 --- a/internal/orchestrator/lease.go +++ b/internal/orchestrator/lease.go @@ -14,7 +14,7 @@ type WorkflowLease struct { expiresAt time.Time } -func (l WorkflowLease) IsExpired(currentTime time.Time) bool { return currentTime.After(l.expiresAt) } +func (wl WorkflowLease) IsExpired(currentTime time.Time) bool { return currentTime.After(wl.expiresAt) } type WorkflowLeaseRepository interface { Add(context.Context, WorkflowLease) error diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 9f50eb5..32b1364 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -5,48 +5,86 @@ import ( "fmt" "github.com/alitto/pond/v2" "github.com/morebec/go-misas/misas" - "github.com/morebec/smallflow/internal/core" + "github.com/morebec/go-misas/muuid" + "github.com/morebec/go-misas/mx" + "github.com/morebec/smallflow/internal/workflowmgmt" ) +const defaultMaxConcurrentWorkers = 1000 + type WorkflowOrchestrator struct { - Clock misas.Clock - API misas.BusinessAPI - LeaseManager WorkflowLeaseManager - Pool pond.Pool + Clock misas.Clock + API misas.BusinessAPI + LeaseManager WorkflowLeaseManager + UUIDGenerator muuid.UUIDGenerator + Pool pond.Pool - ctx context.Context - cancelCtx context.CancelFunc + eventProcessor mx.EventProcessor +} + +func NewWorkflowOrchestrator( + clock misas.Clock, + API misas.BusinessAPI, + leaseManager WorkflowLeaseManager, + uuidGenerator muuid.UUIDGenerator, + eventStore misas.EventStore, + checkpointStore misas.CheckpointStore, +) *WorkflowOrchestrator { + wo := &WorkflowOrchestrator{ + Clock: clock, + API: API, + LeaseManager: leaseManager, + UUIDGenerator: uuidGenerator, + Pool: pond.NewPool(defaultMaxConcurrentWorkers), + } + + wo.eventProcessor = *mx.NewEventProcessor(mx.EventProcessorConfig{ + ID: "workflow-orchestrator", + StreamID: eventStore.GlobalStreamID(), + EventStore: eventStore, + CheckpointStore: checkpointStore, + CommitStrategy: misas.CheckpointCommitStrategyAfterProcessing, + Handler: wo, + }) + + return wo } func (m *WorkflowOrchestrator) Start() { - m.ctx, m.cancelCtx = context.WithCancel(context.Background()) + err := m.eventProcessor.Start() + if err != nil { + panic(err) + } } -func (m *WorkflowOrchestrator) Stop() { m.cancelCtx() } +func (m *WorkflowOrchestrator) Stop() { + m.eventProcessor.Stop() +} func (m *WorkflowOrchestrator) IsRunning() bool { - if m.ctx == nil { - return false - } - return m.ctx.Err() == nil + return m.eventProcessor.IsRunning() } -func (m *WorkflowOrchestrator) HandleEvent(ctx context.Context, event any) error { +func (m *WorkflowOrchestrator) HandleEvent(ctx context.Context, event misas.Event) misas.Error { switch e := event.(type) { - case core.WorkflowTriggeredEvent: - return m.runWorkflow(ctx, e) + case workflowmgmt.WorkflowTriggeredEvent: + err := m.runWorkflow(ctx, e) + if err != nil { + return mx.NewInternalErrorFrom(err) + } + return nil } return nil } -func (m *WorkflowOrchestrator) runWorkflow(_ context.Context, e core.WorkflowTriggeredEvent) error { +func (m *WorkflowOrchestrator) runWorkflow(_ context.Context, e workflowmgmt.WorkflowTriggeredEvent) error { pond.Submit(func() { runner := Worker{ Clock: m.Clock, API: m.API, LeaseManager: m.LeaseManager, - InstanceID: "instance-1", // TODO: generate unique instance ID + InstanceID: m.UUIDGenerator.Generate().String(), } if err := runner.Run(context.Background(), e.WorkflowID, e.RunID); err != nil { fmt.Println("workflow runner error:", err) diff --git a/internal/orchestrator/runner.go b/internal/orchestrator/worker.go similarity index 87% rename from internal/orchestrator/runner.go rename to internal/orchestrator/worker.go index aebde9f..89044ba 100644 --- a/internal/orchestrator/runner.go +++ b/internal/orchestrator/worker.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "github.com/morebec/go-misas/misas" - "github.com/morebec/smallflow/internal/core" + "github.com/morebec/smallflow/internal/workflowmgmt" "time" ) @@ -55,7 +55,8 @@ func (r Worker) acquireLease(ctx context.Context, workflowID, runID string) (con ticker.Stop() cancel() // Release the lease when stopping the heartbeat - if err := r.LeaseManager.Release(ctx, workflowID, runID); err != nil { + // use context.Background() given we just canceled the original context. + if err := r.LeaseManager.Release(context.Background(), workflowID, runID); err != nil { fmt.Printf( "Failed to release lease for workflow run: {workflowID: %s, runID: %s}: %s\n", workflowID, @@ -80,7 +81,7 @@ func (r Worker) Run(ctx context.Context, workflowID, runID string) error { } defer releaseLease() - result := r.API.HandleCommand(ctx, core.RunWorkflowCommand{ + result := r.API.HandleCommand(ctx, workflowmgmt.RunWorkflowCommand{ WorkflowID: workflowID, RunID: runID, }) diff --git a/internal/core/action.go b/internal/workflowmgmt/action.go similarity index 95% rename from internal/core/action.go rename to internal/workflowmgmt/action.go index 1761c3f..7b0b462 100644 --- a/internal/core/action.go +++ b/internal/workflowmgmt/action.go @@ -1,4 +1,4 @@ -package core +package workflowmgmt import "context" diff --git a/internal/workflowmgmt/adapters/run_repo.go b/internal/workflowmgmt/adapters/run_repo.go new file mode 100644 index 0000000..350a2d1 --- /dev/null +++ b/internal/workflowmgmt/adapters/run_repo.go @@ -0,0 +1,100 @@ +package adapters + +import ( + "context" + "github.com/morebec/go-misas/misas" + "github.com/morebec/go-misas/muuid" + "github.com/morebec/go-misas/mx" + "github.com/morebec/smallflow/internal/workflowmgmt" + "github.com/samber/lo" +) + +type EventStoreRunRepository struct { + UUIDGenerator muuid.UUIDGenerator + EventStore misas.EventStore + EventRegistry *mx.MessageRegistry[misas.EventTypeName, misas.Event] +} + +func (r EventStoreRunRepository) Add(ctx context.Context, run *workflowmgmt.Run) misas.Error { + descriptors := lo.Map(run.UncommittedEvents(), func(event misas.Event, _ int) misas.EventDescriptor { + return misas.EventDescriptor{ + ID: r.UUIDGenerator.Generate().String(), + TypeName: event.TypeName(), + Data: event, + } + }) + + err := r.EventStore.AppendToStream( + ctx, + misas.EventStreamID("runs/"+run.ID), + descriptors, + misas.AppendToEventStreamOptions{}. + ExpectNotExist(), + ) + if err != nil { + return mx.NewInternalErrorFrom(err) + } + + run.Commit() + + return nil + +} + +func (r EventStoreRunRepository) Save(ctx context.Context, run *workflowmgmt.Run) misas.Error { + descriptors := lo.Map(run.UncommittedEvents(), func(event misas.Event, _ int) misas.EventDescriptor { + return misas.EventDescriptor{ + ID: r.UUIDGenerator.Generate().String(), + TypeName: event.TypeName(), + Data: event, + } + }) + + err := r.EventStore.AppendToStream( + ctx, + misas.EventStreamID("runs/"+run.ID), + descriptors, + misas.AppendToEventStreamOptions{}, //TODO expected versioning + ) + if err != nil { + return mx.NewInternalErrorFrom(err) + } + + run.Commit() + + return nil +} + +func (r EventStoreRunRepository) FindByID(ctx context.Context, workflowID string, runID string) (*workflowmgmt.Run, misas.Error) { + var run *workflowmgmt.Run + + stream, err := r.EventStore.ReadFromStream( + ctx, + misas.EventStreamID("runs/"+runID), + misas.ReadFromEventStreamOptions{}. + FromStart(). + Forward(), + ) + if err != nil { + if misas.ErrorHasCode(err, misas.ErrEventStreamNotFoundErrorCode) { + return nil, nil + } + + return nil, mx.NewInternalErrorFrom(err) + } + + run = &workflowmgmt.Run{ + ID: workflowmgmt.RunID(runID), + WorkflowID: workflowmgmt.WorkflowID(workflowID), + } + + var events []misas.Event + for _, record := range stream.Events { + events = append(events, record.Data) + } + + run.Apply(events) + + return run, nil + +} diff --git a/internal/workflowmgmt/adapters/workflow_repo.go b/internal/workflowmgmt/adapters/workflow_repo.go new file mode 100644 index 0000000..366b9a7 --- /dev/null +++ b/internal/workflowmgmt/adapters/workflow_repo.go @@ -0,0 +1,94 @@ +package adapters + +import ( + "context" + "fmt" + "github.com/morebec/go-misas/misas" + "github.com/morebec/go-misas/muuid" + "github.com/morebec/go-misas/mx" + "github.com/morebec/smallflow/internal/workflowmgmt" + "github.com/samber/lo" +) + +type EventStoreWorkflowRepository struct { + EventStore misas.EventStore + EventRegistry *mx.MessageRegistry[misas.EventTypeName, misas.Event] + UUIDGenerator muuid.UUIDGenerator +} + +func (r EventStoreWorkflowRepository) FindByID(ctx context.Context, workflowID string) (*workflowmgmt.Workflow, misas.Error) { + stream, err := r.EventStore.ReadFromStream( + ctx, + misas.EventStreamID("workflows/"+workflowID), + misas.ReadFromEventStreamOptions{}. + FromStart(). + Forward(), + ) + if err != nil { + if !misas.ErrorHasCode(err, misas.ErrEventStreamNotFoundErrorCode) { + return nil, mx.NewInternalErrorFrom(err) + } + } + + wf := workflowmgmt.NewWorkflow(workflowmgmt.WorkflowDefinition{ + ID: workflowmgmt.WorkflowID(workflowID), + ConcurrencyLimit: workflowmgmt.ConcurrencyLimitNone, + Steps: []workflowmgmt.StepDefinition{ + { + ID: "step-1", + IgnoreError: false, + Action: workflowmgmt.NewActionFunc("my-action", func(ctx context.Context) *workflowmgmt.WorkflowError { + fmt.Println("Executing my-action") + return nil + }), + }, + { + ID: "step-2", + IgnoreError: false, + Action: workflowmgmt.NewActionFunc("my-action-2", func(ctx context.Context) *workflowmgmt.WorkflowError { + fmt.Println("Executing my-action 2") + return &workflowmgmt.WorkflowError{ + Kind: "internal", + Code: "not_implemented", + Message: "this action is not implemented", + Details: map[string]any{"action_id": "my-action-2"}, + } + //return nil + }), + }, + }, + }, false) + + var events []misas.Event + for _, record := range stream.Events { + events = append(events, record.Data) + } + + wf.Apply(events) + + return wf, nil +} + +func (r EventStoreWorkflowRepository) Save(ctx context.Context, wf *workflowmgmt.Workflow) misas.Error { + descriptors := lo.Map(wf.UncommittedEvents(), func(event misas.Event, _ int) misas.EventDescriptor { + return misas.EventDescriptor{ + ID: r.UUIDGenerator.Generate().String(), + TypeName: event.TypeName(), + Data: event, + } + }) + + err := r.EventStore.AppendToStream( + ctx, + misas.EventStreamID("workflows/"+wf.ID()), + descriptors, + misas.AppendToEventStreamOptions{}, //TODO expected versioning + ) + if err != nil { + return mx.NewInternalErrorFrom(err) + } + + wf.Commit() + + return nil +} diff --git a/internal/core/api.go b/internal/workflowmgmt/api.go similarity index 99% rename from internal/core/api.go rename to internal/workflowmgmt/api.go index 7e725a4..dfba2af 100644 --- a/internal/core/api.go +++ b/internal/workflowmgmt/api.go @@ -1,4 +1,4 @@ -package core +package workflowmgmt import ( "fmt" diff --git a/internal/core/run.go b/internal/workflowmgmt/run.go similarity index 94% rename from internal/core/run.go rename to internal/workflowmgmt/run.go index dbc2144..fa8b627 100644 --- a/internal/core/run.go +++ b/internal/workflowmgmt/run.go @@ -1,4 +1,4 @@ -package core +package workflowmgmt import ( "fmt" @@ -25,7 +25,7 @@ type Run struct { CurrentStepID StepID Steps map[StepID]*StepRun - events []any + events []misas.Event Status WorkflowStatus } @@ -129,6 +129,7 @@ func (r *Run) End(currentTime time.Time) { StartedAt: r.StartedAt, EndedAt: currentTime, Status: string(workflowStatus), + Errors: r.Errors(), }) } @@ -143,7 +144,7 @@ func (r *Run) Errors() map[string]*WorkflowError { return stepErrors } -func (r *Run) Apply(events []any) { +func (r *Run) Apply(events []misas.Event) { for _, event := range events { switch e := event.(type) { case WorkflowStartedEvent: @@ -175,11 +176,11 @@ func (r *Run) Apply(events []any) { } } -func (r *Run) UncommittedEvents() []any { return r.events } +func (r *Run) UncommittedEvents() []misas.Event { return r.events } -func (r *Run) record(event any) { +func (r *Run) record(event misas.Event) { r.events = append(r.events, event) - r.Apply([]any{event}) + r.Apply([]misas.Event{event}) } func (r *Run) Commit() { r.events = nil } diff --git a/internal/core/runner.go b/internal/workflowmgmt/runner.go similarity index 99% rename from internal/core/runner.go rename to internal/workflowmgmt/runner.go index 2b04c25..2593211 100644 --- a/internal/core/runner.go +++ b/internal/workflowmgmt/runner.go @@ -1,4 +1,4 @@ -package core +package workflowmgmt import ( "context" diff --git a/internal/core/spi.go b/internal/workflowmgmt/spi.go similarity index 95% rename from internal/core/spi.go rename to internal/workflowmgmt/spi.go index 2dc0a2c..6683398 100644 --- a/internal/core/spi.go +++ b/internal/workflowmgmt/spi.go @@ -1,4 +1,4 @@ -package core +package workflowmgmt import ( "context" diff --git a/internal/core/util.go b/internal/workflowmgmt/util.go similarity index 95% rename from internal/core/util.go rename to internal/workflowmgmt/util.go index c51e376..dec9572 100644 --- a/internal/core/util.go +++ b/internal/workflowmgmt/util.go @@ -1,4 +1,4 @@ -package core +package workflowmgmt import "github.com/morebec/go-misas/misas" diff --git a/internal/core/workflow.go b/internal/workflowmgmt/workflow.go similarity index 89% rename from internal/core/workflow.go rename to internal/workflowmgmt/workflow.go index dfdd6a6..d5fcabf 100644 --- a/internal/core/workflow.go +++ b/internal/workflowmgmt/workflow.go @@ -1,4 +1,4 @@ -package core +package workflowmgmt import ( "fmt" @@ -16,7 +16,7 @@ type Workflow struct { runIds map[RunID]struct{} activeRuns map[RunID]struct{} - events []any + events []misas.Event } func NewWorkflow(d WorkflowDefinition, enabled bool) *Workflow { @@ -76,14 +76,14 @@ func (w *Workflow) Disable(now time.Time) { }) } -func (w *Workflow) record(event any) { - w.Apply([]any{event}) +func (w *Workflow) record(event misas.Event) { + w.Apply([]misas.Event{event}) w.events = append(w.events, event) } -func (w *Workflow) UncommittedEvents() []any { return w.events } +func (w *Workflow) UncommittedEvents() []misas.Event { return w.events } -func (w *Workflow) Apply(events []any) { +func (w *Workflow) Apply(events []misas.Event) { for _, event := range events { switch e := event.(type) { case WorkflowTriggeredEvent: diff --git a/internal/core/workflow_definition.go b/internal/workflowmgmt/workflow_definition.go similarity index 93% rename from internal/core/workflow_definition.go rename to internal/workflowmgmt/workflow_definition.go index 542e8cc..d8223b0 100644 --- a/internal/core/workflow_definition.go +++ b/internal/workflowmgmt/workflow_definition.go @@ -1,4 +1,4 @@ -package core +package workflowmgmt type StepID string diff --git a/internal/core/workflow_disable.go b/internal/workflowmgmt/workflow_disable.go similarity index 96% rename from internal/core/workflow_disable.go rename to internal/workflowmgmt/workflow_disable.go index aea69b7..4084796 100644 --- a/internal/core/workflow_disable.go +++ b/internal/workflowmgmt/workflow_disable.go @@ -1,4 +1,4 @@ -package core +package workflowmgmt import ( "context" diff --git a/internal/core/workflow_enable.go b/internal/workflowmgmt/workflow_enable.go similarity index 96% rename from internal/core/workflow_enable.go rename to internal/workflowmgmt/workflow_enable.go index 6b9b9db..2c540cf 100644 --- a/internal/core/workflow_enable.go +++ b/internal/workflowmgmt/workflow_enable.go @@ -1,4 +1,4 @@ -package core +package workflowmgmt import ( "context" diff --git a/internal/core/workflow_resume.go b/internal/workflowmgmt/workflow_resume.go similarity index 97% rename from internal/core/workflow_resume.go rename to internal/workflowmgmt/workflow_resume.go index 0adc48b..0da1c0f 100644 --- a/internal/core/workflow_resume.go +++ b/internal/workflowmgmt/workflow_resume.go @@ -1,4 +1,4 @@ -package core +package workflowmgmt import ( "context" diff --git a/internal/core/workflow_run.go b/internal/workflowmgmt/workflow_run.go similarity index 97% rename from internal/core/workflow_run.go rename to internal/workflowmgmt/workflow_run.go index bd615b6..cc4363b 100644 --- a/internal/core/workflow_run.go +++ b/internal/workflowmgmt/workflow_run.go @@ -1,4 +1,4 @@ -package core +package workflowmgmt import ( "context" diff --git a/internal/core/workflow_trigger.go b/internal/workflowmgmt/workflow_trigger.go similarity index 97% rename from internal/core/workflow_trigger.go rename to internal/workflowmgmt/workflow_trigger.go index 556401e..e56bfc0 100644 --- a/internal/core/workflow_trigger.go +++ b/internal/workflowmgmt/workflow_trigger.go @@ -1,4 +1,4 @@ -package core +package workflowmgmt import ( "context" diff --git a/specs/business/workflowmgmt.def.hcl b/specs/business/workflowmgmt.def.hcl new file mode 100644 index 0000000..934c368 --- /dev/null +++ b/specs/business/workflowmgmt.def.hcl @@ -0,0 +1,295 @@ +subsystem "workflowmgmt" { + description = "Business subsystem responsible for core business logic related to managing workflows." + type = "business" +} + + +command "workflowmgmt.EnableWorkflow" { + description = "EnableWorkflowCommand represents a command to enable a workflow." + field "WorkflowID" { + description = "ID of the workflow to enable." + type = "identifier" + } +} + +event "workflowmgmt.WorkflowEnabled" { + description = "Event emitted when a workflow is successfully enabled." + field "WorkflowID" { + description = "ID of the workflow that was enabled." + type = "identifier" + } + field "EnabledAt" { + description = "Timestamp when the workflow was enabled." + type = "datetime" + } +} + +command "workflowmgmt.DisableWorkflow" { + description = "DisableWorkflowCommand represents a command to disable a workflow." + field "WorkflowID" { + description = "ID of the workflow to disable." + type = "identifier" + } +} + +event "workflowmgmt.WorkflowDisabled" { + description = "Event emitted when a workflow is successfully disabled." + field "WorkflowID" { + description = "ID of the workflow that was disabled." + type = "identifier" + } + field "DisabledAt" { + description = "Timestamp when the workflow was disabled." + type = "datetime" + } +} + +command "workflowmgmt.TriggerWorkflow" { + description = < Date: Sun, 19 Oct 2025 12:05:36 -0400 Subject: [PATCH 4/6] migrate .golangci.yml --- .golangci.yml | 48 ++++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 2676388..a73cf62 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,31 +1,35 @@ +version: "2" +run: + tests: true linters: enable: - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - unused - - gofmt - - goimports - misspell - unconvert - unparam - whitespace - -linters-settings: - errcheck: - check-type-assertions: true - check-blank: true - - govet: - check-shadowing: true - -run: - timeout: 5m - tests: true - + settings: + errcheck: + check-type-assertions: true + check-blank: true + govet: + enable: + - shadow + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ issues: - exclude-use-default: false max-issues-per-linter: 0 max-same-issues: 0 +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ From 6ce92ddd375f54656b384d369f5e133f9a9f30ae Mon Sep 17 00:00:00 2001 From: = Date: Sun, 19 Oct 2025 12:13:52 -0400 Subject: [PATCH 5/6] Fix linting issues --- Taskfile.yml | 5 +++ cmd/smallflow/main.go | 31 ++++++------------- .../orchestrator/adapters/lease_inmemory.go | 3 +- .../orchestrator/adapters/lease_postgres.go | 2 +- internal/orchestrator/lease.go | 3 +- internal/orchestrator/orchestrator.go | 11 +++---- internal/orchestrator/worker.go | 3 +- internal/workflowmgmt/adapters/run_repo.go | 4 +-- .../workflowmgmt/adapters/workflow_repo.go | 2 +- internal/workflowmgmt/api.go | 3 +- internal/workflowmgmt/run.go | 14 ++------- internal/workflowmgmt/runner.go | 1 + internal/workflowmgmt/spi.go | 1 + internal/workflowmgmt/workflow.go | 3 +- internal/workflowmgmt/workflow_disable.go | 1 + internal/workflowmgmt/workflow_enable.go | 1 + internal/workflowmgmt/workflow_resume.go | 1 + internal/workflowmgmt/workflow_run.go | 2 +- internal/workflowmgmt/workflow_trigger.go | 1 + 19 files changed, 42 insertions(+), 50 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index faa2d7c..6a567b5 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -41,6 +41,11 @@ tasks: cmds: - golangci-lint run ./... + lint-fix: + desc: Run linter + cmds: + - golangci-lint run ./... --fix + fmt: desc: Format code cmds: diff --git a/cmd/smallflow/main.go b/cmd/smallflow/main.go index 5a9ad88..edf8e89 100644 --- a/cmd/smallflow/main.go +++ b/cmd/smallflow/main.go @@ -3,6 +3,8 @@ package main import ( "context" "fmt" + "time" + "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/mpostgres" "github.com/morebec/go-misas/muuid" @@ -11,7 +13,6 @@ import ( adapters2 "github.com/morebec/smallflow/internal/orchestrator/adapters" "github.com/morebec/smallflow/internal/workflowmgmt" "github.com/morebec/smallflow/internal/workflowmgmt/adapters" - "time" ) func main() { @@ -30,26 +31,22 @@ func main() { panic(err) } - eventRegistry := mx.NewMessageRegistry[misas.EventTypeName, misas.Event]() + mx.EventRegistry.Register(workflowmgmt.WorkflowEnabledEventTypeName, workflowmgmt.WorkflowEnabledEvent{}) + mx.EventRegistry.Register(workflowmgmt.WorkflowDisabledEventTypeName, workflowmgmt.WorkflowDisabledEvent{}) + mx.EventRegistry.Register(workflowmgmt.WorkflowTriggeredEventTypeName, workflowmgmt.WorkflowTriggeredEvent{}) + mx.EventRegistry.Register(workflowmgmt.WorkflowStartedEventTypeName, workflowmgmt.WorkflowStartedEvent{}) + mx.EventRegistry.Register(workflowmgmt.WorkflowEndedEventTypeName, workflowmgmt.WorkflowEndedEvent{}) + mx.EventRegistry.Register(workflowmgmt.StepStartedEventTypeName, workflowmgmt.StepStartedEvent{}) + mx.EventRegistry.Register(workflowmgmt.StepEndedEventTypeName, workflowmgmt.StepEndedEvent{}) - eventRegistry.Register(workflowmgmt.WorkflowEnabledEventTypeName, workflowmgmt.WorkflowEnabledEvent{}) - eventRegistry.Register(workflowmgmt.WorkflowDisabledEventTypeName, workflowmgmt.WorkflowDisabledEvent{}) - eventRegistry.Register(workflowmgmt.WorkflowTriggeredEventTypeName, workflowmgmt.WorkflowTriggeredEvent{}) - eventRegistry.Register(workflowmgmt.WorkflowStartedEventTypeName, workflowmgmt.WorkflowStartedEvent{}) - eventRegistry.Register(workflowmgmt.WorkflowEndedEventTypeName, workflowmgmt.WorkflowEndedEvent{}) - eventRegistry.Register(workflowmgmt.StepStartedEventTypeName, workflowmgmt.StepStartedEvent{}) - eventRegistry.Register(workflowmgmt.StepEndedEventTypeName, workflowmgmt.StepEndedEvent{}) - - eventStore = mx.NewEventStoreDeserializerDecorator(eventStore, eventRegistry) + eventStore = mx.NewEventStoreDeserializerDecorator(eventStore) workflowRepo := &adapters.EventStoreWorkflowRepository{ EventStore: eventStore, - EventRegistry: eventRegistry, UUIDGenerator: muuid.NewRandomUUIDGenerator(), } runRepo := &adapters.EventStoreRunRepository{ EventStore: eventStore, - EventRegistry: eventRegistry, UUIDGenerator: muuid.NewRandomUUIDGenerator(), } @@ -109,11 +106,3 @@ func main() { fmt.Printf("Event %d: %T → %+v\n", i, event, event) } } - -func registerActions() { - //actionRegistry := definition.ActionRegistry{} - //actionRegistry.Register(definition.NewActionFunc("my_action", func(ctx definition.Action) *definition.ActionError { - // fmt.Println("Hello, World!") - // return nil - //})) -} diff --git a/internal/orchestrator/adapters/lease_inmemory.go b/internal/orchestrator/adapters/lease_inmemory.go index fc5649c..5bcdf60 100644 --- a/internal/orchestrator/adapters/lease_inmemory.go +++ b/internal/orchestrator/adapters/lease_inmemory.go @@ -2,8 +2,9 @@ package adapters import ( "context" - "github.com/morebec/smallflow/internal/orchestrator" "sync" + + "github.com/morebec/smallflow/internal/orchestrator" ) type InMemoryWorkflowLeaseRepository struct { diff --git a/internal/orchestrator/adapters/lease_postgres.go b/internal/orchestrator/adapters/lease_postgres.go index d6a61f7..766a36a 100644 --- a/internal/orchestrator/adapters/lease_postgres.go +++ b/internal/orchestrator/adapters/lease_postgres.go @@ -2,6 +2,7 @@ package adapters import ( "context" + "github.com/morebec/go-misas/mpostgres" "github.com/morebec/smallflow/internal/orchestrator" ) @@ -57,7 +58,6 @@ func (r PostgresWorkflowLeaseRepository) Update(ctx context.Context, lease orche } return nil - } func (r PostgresWorkflowLeaseRepository) Remove(ctx context.Context, workflowID string, runID string) error { diff --git a/internal/orchestrator/lease.go b/internal/orchestrator/lease.go index 8b096e7..c0a5a4f 100644 --- a/internal/orchestrator/lease.go +++ b/internal/orchestrator/lease.go @@ -2,8 +2,9 @@ package orchestrator import ( "context" - "github.com/morebec/go-misas/misas" "time" + + "github.com/morebec/go-misas/misas" ) const defaultLeaseDuration = 5 * time.Minute diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 32b1364..e169b25 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -3,6 +3,7 @@ package orchestrator import ( "context" "fmt" + "github.com/alitto/pond/v2" "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/muuid" @@ -68,18 +69,15 @@ func (m *WorkflowOrchestrator) IsRunning() bool { func (m *WorkflowOrchestrator) HandleEvent(ctx context.Context, event misas.Event) misas.Error { switch e := event.(type) { case workflowmgmt.WorkflowTriggeredEvent: - err := m.runWorkflow(ctx, e) - if err != nil { - return mx.NewInternalErrorFrom(err) - } + m.runWorkflow(ctx, e) return nil } return nil } -func (m *WorkflowOrchestrator) runWorkflow(_ context.Context, e workflowmgmt.WorkflowTriggeredEvent) error { - pond.Submit(func() { +func (m *WorkflowOrchestrator) runWorkflow(_ context.Context, e workflowmgmt.WorkflowTriggeredEvent) { + m.Pool.Submit(func() { runner := Worker{ Clock: m.Clock, API: m.API, @@ -90,5 +88,4 @@ func (m *WorkflowOrchestrator) runWorkflow(_ context.Context, e workflowmgmt.Wor fmt.Println("workflow runner error:", err) } }) - return nil } diff --git a/internal/orchestrator/worker.go b/internal/orchestrator/worker.go index 89044ba..48b926f 100644 --- a/internal/orchestrator/worker.go +++ b/internal/orchestrator/worker.go @@ -3,9 +3,10 @@ package orchestrator import ( "context" "fmt" + "time" + "github.com/morebec/go-misas/misas" "github.com/morebec/smallflow/internal/workflowmgmt" - "time" ) const defaultLeaseHeartbeatDuration = time.Second * 30 diff --git a/internal/workflowmgmt/adapters/run_repo.go b/internal/workflowmgmt/adapters/run_repo.go index 350a2d1..722040b 100644 --- a/internal/workflowmgmt/adapters/run_repo.go +++ b/internal/workflowmgmt/adapters/run_repo.go @@ -2,6 +2,7 @@ package adapters import ( "context" + "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/muuid" "github.com/morebec/go-misas/mx" @@ -12,7 +13,6 @@ import ( type EventStoreRunRepository struct { UUIDGenerator muuid.UUIDGenerator EventStore misas.EventStore - EventRegistry *mx.MessageRegistry[misas.EventTypeName, misas.Event] } func (r EventStoreRunRepository) Add(ctx context.Context, run *workflowmgmt.Run) misas.Error { @@ -38,7 +38,6 @@ func (r EventStoreRunRepository) Add(ctx context.Context, run *workflowmgmt.Run) run.Commit() return nil - } func (r EventStoreRunRepository) Save(ctx context.Context, run *workflowmgmt.Run) misas.Error { @@ -96,5 +95,4 @@ func (r EventStoreRunRepository) FindByID(ctx context.Context, workflowID string run.Apply(events) return run, nil - } diff --git a/internal/workflowmgmt/adapters/workflow_repo.go b/internal/workflowmgmt/adapters/workflow_repo.go index 366b9a7..ff55858 100644 --- a/internal/workflowmgmt/adapters/workflow_repo.go +++ b/internal/workflowmgmt/adapters/workflow_repo.go @@ -3,6 +3,7 @@ package adapters import ( "context" "fmt" + "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/muuid" "github.com/morebec/go-misas/mx" @@ -12,7 +13,6 @@ import ( type EventStoreWorkflowRepository struct { EventStore misas.EventStore - EventRegistry *mx.MessageRegistry[misas.EventTypeName, misas.Event] UUIDGenerator muuid.UUIDGenerator } diff --git a/internal/workflowmgmt/api.go b/internal/workflowmgmt/api.go index dfba2af..c852a9f 100644 --- a/internal/workflowmgmt/api.go +++ b/internal/workflowmgmt/api.go @@ -2,10 +2,11 @@ package workflowmgmt import ( "fmt" + "time" + "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/muuid" "github.com/morebec/go-misas/mx" - "time" ) func NewSubsystem( diff --git a/internal/workflowmgmt/run.go b/internal/workflowmgmt/run.go index fa8b627..9f5f5fd 100644 --- a/internal/workflowmgmt/run.go +++ b/internal/workflowmgmt/run.go @@ -2,9 +2,10 @@ package workflowmgmt import ( "fmt" + "time" + "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/mx" - "time" ) type WorkflowStatus string @@ -106,23 +107,14 @@ func (r *Run) End(currentTime time.Time) { return } - // Collect step errors - var stepErrors map[string]WorkflowError workflowStatus := WorkflowStatusSucceeded for _, s := range r.Steps { - if s.Error != nil { - if stepErrors == nil { - stepErrors = make(map[string]WorkflowError) - } - stepErrors[string(s.ID)] = *s.Error - } if s.Status == StepStatusFailed { workflowStatus = WorkflowStatusFailed + break } } - // Compute Status - r.record(WorkflowEndedEvent{ RunID: string(r.ID), WorkflowID: string(r.WorkflowID), diff --git a/internal/workflowmgmt/runner.go b/internal/workflowmgmt/runner.go index 2593211..8da24d0 100644 --- a/internal/workflowmgmt/runner.go +++ b/internal/workflowmgmt/runner.go @@ -3,6 +3,7 @@ package workflowmgmt import ( "context" "fmt" + "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/mx" ) diff --git a/internal/workflowmgmt/spi.go b/internal/workflowmgmt/spi.go index 6683398..4321a83 100644 --- a/internal/workflowmgmt/spi.go +++ b/internal/workflowmgmt/spi.go @@ -2,6 +2,7 @@ package workflowmgmt import ( "context" + "github.com/morebec/go-misas/misas" ) diff --git a/internal/workflowmgmt/workflow.go b/internal/workflowmgmt/workflow.go index d5fcabf..a2bffdb 100644 --- a/internal/workflowmgmt/workflow.go +++ b/internal/workflowmgmt/workflow.go @@ -2,9 +2,10 @@ package workflowmgmt import ( "fmt" + "time" + "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/mx" - "time" ) type WorkflowID string diff --git a/internal/workflowmgmt/workflow_disable.go b/internal/workflowmgmt/workflow_disable.go index 4084796..b8e4ec4 100644 --- a/internal/workflowmgmt/workflow_disable.go +++ b/internal/workflowmgmt/workflow_disable.go @@ -2,6 +2,7 @@ package workflowmgmt import ( "context" + "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/mx" ) diff --git a/internal/workflowmgmt/workflow_enable.go b/internal/workflowmgmt/workflow_enable.go index 2c540cf..5fa9bf5 100644 --- a/internal/workflowmgmt/workflow_enable.go +++ b/internal/workflowmgmt/workflow_enable.go @@ -2,6 +2,7 @@ package workflowmgmt import ( "context" + "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/mx" ) diff --git a/internal/workflowmgmt/workflow_resume.go b/internal/workflowmgmt/workflow_resume.go index 0da1c0f..217f59c 100644 --- a/internal/workflowmgmt/workflow_resume.go +++ b/internal/workflowmgmt/workflow_resume.go @@ -2,6 +2,7 @@ package workflowmgmt import ( "context" + "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/mx" ) diff --git a/internal/workflowmgmt/workflow_run.go b/internal/workflowmgmt/workflow_run.go index cc4363b..aff5222 100644 --- a/internal/workflowmgmt/workflow_run.go +++ b/internal/workflowmgmt/workflow_run.go @@ -2,6 +2,7 @@ package workflowmgmt import ( "context" + "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/mx" ) @@ -33,5 +34,4 @@ func (h RunWorkflowCommandHandler) Handle(ctx context.Context, cmd RunWorkflowCo } return misas.CommandResult{Payload: report} - } diff --git a/internal/workflowmgmt/workflow_trigger.go b/internal/workflowmgmt/workflow_trigger.go index e56bfc0..aa40512 100644 --- a/internal/workflowmgmt/workflow_trigger.go +++ b/internal/workflowmgmt/workflow_trigger.go @@ -2,6 +2,7 @@ package workflowmgmt import ( "context" + "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/muuid" "github.com/morebec/go-misas/mx" From 2b80bf1a6235d42d0f0998dbbc95679cada59344 Mon Sep 17 00:00:00 2001 From: = Date: Sun, 19 Oct 2025 23:50:01 -0400 Subject: [PATCH 6/6] reorganize packages --- cmd/smallflow/main.go | 17 ++++++------ go.mod | 2 +- go.sum | 1 + .../{ => application}/orchestrator/lease.go | 0 .../orchestrator/orchestrator.go | 2 +- .../{ => application}/orchestrator/worker.go | 2 +- .../{ => business}/workflowmgmt/action.go | 0 internal/{ => business}/workflowmgmt/api.go | 0 internal/{ => business}/workflowmgmt/run.go | 0 .../{ => business}/workflowmgmt/runner.go | 0 internal/{ => business}/workflowmgmt/spi.go | 0 internal/{ => business}/workflowmgmt/util.go | 0 .../{ => business}/workflowmgmt/workflow.go | 0 .../workflowmgmt/workflow_definition.go | 0 .../workflowmgmt/workflow_disable.go | 0 .../workflowmgmt/workflow_enable.go | 0 .../workflowmgmt/workflow_resume.go | 0 .../workflowmgmt/workflow_run.go | 0 .../workflowmgmt/workflow_trigger.go | 0 .../inmemory}/lease_inmemory.go | 3 +-- .../postgres/lease_repo.go} | 26 +++++++++---------- .../postgres}/run_repo.go | 4 +-- .../postgres}/workflow_repo.go | 4 +-- 23 files changed, 30 insertions(+), 31 deletions(-) rename internal/{ => application}/orchestrator/lease.go (100%) rename internal/{ => application}/orchestrator/orchestrator.go (97%) rename internal/{ => application}/orchestrator/worker.go (97%) rename internal/{ => business}/workflowmgmt/action.go (100%) rename internal/{ => business}/workflowmgmt/api.go (100%) rename internal/{ => business}/workflowmgmt/run.go (100%) rename internal/{ => business}/workflowmgmt/runner.go (100%) rename internal/{ => business}/workflowmgmt/spi.go (100%) rename internal/{ => business}/workflowmgmt/util.go (100%) rename internal/{ => business}/workflowmgmt/workflow.go (100%) rename internal/{ => business}/workflowmgmt/workflow_definition.go (100%) rename internal/{ => business}/workflowmgmt/workflow_disable.go (100%) rename internal/{ => business}/workflowmgmt/workflow_enable.go (100%) rename internal/{ => business}/workflowmgmt/workflow_resume.go (100%) rename internal/{ => business}/workflowmgmt/workflow_run.go (100%) rename internal/{ => business}/workflowmgmt/workflow_trigger.go (100%) rename internal/{orchestrator/adapters => integration/inmemory}/lease_inmemory.go (94%) rename internal/{orchestrator/adapters/lease_postgres.go => integration/postgres/lease_repo.go} (54%) rename internal/{workflowmgmt/adapters => integration/postgres}/run_repo.go (96%) rename internal/{workflowmgmt/adapters => integration/postgres}/workflow_repo.go (96%) diff --git a/cmd/smallflow/main.go b/cmd/smallflow/main.go index edf8e89..007a72d 100644 --- a/cmd/smallflow/main.go +++ b/cmd/smallflow/main.go @@ -3,16 +3,15 @@ package main import ( "context" "fmt" + orchestrator2 "github.com/morebec/smallflow/internal/application/orchestrator" + "github.com/morebec/smallflow/internal/business/workflowmgmt" + "github.com/morebec/smallflow/internal/integration/postgres" "time" "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/mpostgres" "github.com/morebec/go-misas/muuid" "github.com/morebec/go-misas/mx" - "github.com/morebec/smallflow/internal/orchestrator" - adapters2 "github.com/morebec/smallflow/internal/orchestrator/adapters" - "github.com/morebec/smallflow/internal/workflowmgmt" - "github.com/morebec/smallflow/internal/workflowmgmt/adapters" ) func main() { @@ -41,22 +40,22 @@ func main() { eventStore = mx.NewEventStoreDeserializerDecorator(eventStore) - workflowRepo := &adapters.EventStoreWorkflowRepository{ + workflowRepo := &postgres.EventStoreWorkflowRepository{ EventStore: eventStore, UUIDGenerator: muuid.NewRandomUUIDGenerator(), } - runRepo := &adapters.EventStoreRunRepository{ + runRepo := &postgres.EventStoreRunRepository{ EventStore: eventStore, UUIDGenerator: muuid.NewRandomUUIDGenerator(), } api := workflowmgmt.NewSubsystem(clock, workflowRepo, runRepo, muuid.NewRandomUUIDGenerator()).API - workflowLeaseRepository, err := adapters2.NewPostgresWorkflowLeaseRepository(dbConn) + workflowLeaseRepository, err := postgres.NewWorkflowLeaseRepository(dbConn) if err != nil { panic(err) } - leaseManager := orchestrator.WorkflowLeaseManager{ + leaseManager := orchestrator2.WorkflowLeaseManager{ Clock: clock, Repository: workflowLeaseRepository, } @@ -65,7 +64,7 @@ func main() { if err != nil { panic(err) } - orch := orchestrator.NewWorkflowOrchestrator( + orch := orchestrator2.NewWorkflowOrchestrator( clock, api, leaseManager, diff --git a/go.mod b/go.mod index 9c64181..0562285 100644 --- a/go.mod +++ b/go.mod @@ -15,5 +15,5 @@ require ( github.com/lib/pq v1.10.9 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/text v0.28.0 // indirect ) diff --git a/go.sum b/go.sum index 7807215..b114515 100644 --- a/go.sum +++ b/go.sum @@ -44,5 +44,6 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/orchestrator/lease.go b/internal/application/orchestrator/lease.go similarity index 100% rename from internal/orchestrator/lease.go rename to internal/application/orchestrator/lease.go diff --git a/internal/orchestrator/orchestrator.go b/internal/application/orchestrator/orchestrator.go similarity index 97% rename from internal/orchestrator/orchestrator.go rename to internal/application/orchestrator/orchestrator.go index e169b25..ebbaed7 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/application/orchestrator/orchestrator.go @@ -3,12 +3,12 @@ package orchestrator import ( "context" "fmt" + "github.com/morebec/smallflow/internal/business/workflowmgmt" "github.com/alitto/pond/v2" "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/muuid" "github.com/morebec/go-misas/mx" - "github.com/morebec/smallflow/internal/workflowmgmt" ) const defaultMaxConcurrentWorkers = 1000 diff --git a/internal/orchestrator/worker.go b/internal/application/orchestrator/worker.go similarity index 97% rename from internal/orchestrator/worker.go rename to internal/application/orchestrator/worker.go index 48b926f..1849d8e 100644 --- a/internal/orchestrator/worker.go +++ b/internal/application/orchestrator/worker.go @@ -3,10 +3,10 @@ package orchestrator import ( "context" "fmt" + "github.com/morebec/smallflow/internal/business/workflowmgmt" "time" "github.com/morebec/go-misas/misas" - "github.com/morebec/smallflow/internal/workflowmgmt" ) const defaultLeaseHeartbeatDuration = time.Second * 30 diff --git a/internal/workflowmgmt/action.go b/internal/business/workflowmgmt/action.go similarity index 100% rename from internal/workflowmgmt/action.go rename to internal/business/workflowmgmt/action.go diff --git a/internal/workflowmgmt/api.go b/internal/business/workflowmgmt/api.go similarity index 100% rename from internal/workflowmgmt/api.go rename to internal/business/workflowmgmt/api.go diff --git a/internal/workflowmgmt/run.go b/internal/business/workflowmgmt/run.go similarity index 100% rename from internal/workflowmgmt/run.go rename to internal/business/workflowmgmt/run.go diff --git a/internal/workflowmgmt/runner.go b/internal/business/workflowmgmt/runner.go similarity index 100% rename from internal/workflowmgmt/runner.go rename to internal/business/workflowmgmt/runner.go diff --git a/internal/workflowmgmt/spi.go b/internal/business/workflowmgmt/spi.go similarity index 100% rename from internal/workflowmgmt/spi.go rename to internal/business/workflowmgmt/spi.go diff --git a/internal/workflowmgmt/util.go b/internal/business/workflowmgmt/util.go similarity index 100% rename from internal/workflowmgmt/util.go rename to internal/business/workflowmgmt/util.go diff --git a/internal/workflowmgmt/workflow.go b/internal/business/workflowmgmt/workflow.go similarity index 100% rename from internal/workflowmgmt/workflow.go rename to internal/business/workflowmgmt/workflow.go diff --git a/internal/workflowmgmt/workflow_definition.go b/internal/business/workflowmgmt/workflow_definition.go similarity index 100% rename from internal/workflowmgmt/workflow_definition.go rename to internal/business/workflowmgmt/workflow_definition.go diff --git a/internal/workflowmgmt/workflow_disable.go b/internal/business/workflowmgmt/workflow_disable.go similarity index 100% rename from internal/workflowmgmt/workflow_disable.go rename to internal/business/workflowmgmt/workflow_disable.go diff --git a/internal/workflowmgmt/workflow_enable.go b/internal/business/workflowmgmt/workflow_enable.go similarity index 100% rename from internal/workflowmgmt/workflow_enable.go rename to internal/business/workflowmgmt/workflow_enable.go diff --git a/internal/workflowmgmt/workflow_resume.go b/internal/business/workflowmgmt/workflow_resume.go similarity index 100% rename from internal/workflowmgmt/workflow_resume.go rename to internal/business/workflowmgmt/workflow_resume.go diff --git a/internal/workflowmgmt/workflow_run.go b/internal/business/workflowmgmt/workflow_run.go similarity index 100% rename from internal/workflowmgmt/workflow_run.go rename to internal/business/workflowmgmt/workflow_run.go diff --git a/internal/workflowmgmt/workflow_trigger.go b/internal/business/workflowmgmt/workflow_trigger.go similarity index 100% rename from internal/workflowmgmt/workflow_trigger.go rename to internal/business/workflowmgmt/workflow_trigger.go diff --git a/internal/orchestrator/adapters/lease_inmemory.go b/internal/integration/inmemory/lease_inmemory.go similarity index 94% rename from internal/orchestrator/adapters/lease_inmemory.go rename to internal/integration/inmemory/lease_inmemory.go index 5bcdf60..0bd31ad 100644 --- a/internal/orchestrator/adapters/lease_inmemory.go +++ b/internal/integration/inmemory/lease_inmemory.go @@ -2,9 +2,8 @@ package adapters import ( "context" + "github.com/morebec/smallflow/internal/application/orchestrator" "sync" - - "github.com/morebec/smallflow/internal/orchestrator" ) type InMemoryWorkflowLeaseRepository struct { diff --git a/internal/orchestrator/adapters/lease_postgres.go b/internal/integration/postgres/lease_repo.go similarity index 54% rename from internal/orchestrator/adapters/lease_postgres.go rename to internal/integration/postgres/lease_repo.go index 766a36a..fbafcf5 100644 --- a/internal/orchestrator/adapters/lease_postgres.go +++ b/internal/integration/postgres/lease_repo.go @@ -1,36 +1,36 @@ -package adapters +package postgres import ( "context" + "github.com/morebec/smallflow/internal/application/orchestrator" "github.com/morebec/go-misas/mpostgres" - "github.com/morebec/smallflow/internal/orchestrator" ) -type PostgresWorkflowLeaseRepository struct { +type WorkflowLeaseRepository struct { conn mpostgres.DB collection mpostgres.Collection } -func NewPostgresWorkflowLeaseRepository(conn mpostgres.DB) (PostgresWorkflowLeaseRepository, error) { +func NewWorkflowLeaseRepository(conn mpostgres.DB) (WorkflowLeaseRepository, error) { ctx := context.Background() docStore, err := mpostgres.NewDocumentStore(ctx, conn) if err != nil { - return PostgresWorkflowLeaseRepository{}, err + return WorkflowLeaseRepository{}, err } collection, err := docStore.Collection("workflow_leases") if err != nil { - return PostgresWorkflowLeaseRepository{}, err + return WorkflowLeaseRepository{}, err } if err := collection.Create(ctx); err != nil { - return PostgresWorkflowLeaseRepository{}, err + return WorkflowLeaseRepository{}, err } - return PostgresWorkflowLeaseRepository{conn: conn, collection: collection}, nil + return WorkflowLeaseRepository{conn: conn, collection: collection}, nil } -func (r PostgresWorkflowLeaseRepository) Add(ctx context.Context, lease orchestrator.WorkflowLease) error { +func (r WorkflowLeaseRepository) Add(ctx context.Context, lease orchestrator.WorkflowLease) error { doc, err := mpostgres.NewDocument(r.workflowLeasID(lease.WorkflowID, lease.RunID), lease) if err != nil { return err @@ -43,11 +43,11 @@ func (r PostgresWorkflowLeaseRepository) Add(ctx context.Context, lease orchestr return nil } -func (r PostgresWorkflowLeaseRepository) workflowLeasID(workflowID, runID string) string { +func (r WorkflowLeaseRepository) workflowLeasID(workflowID, runID string) string { return workflowID + "/" + runID } -func (r PostgresWorkflowLeaseRepository) Update(ctx context.Context, lease orchestrator.WorkflowLease) error { +func (r WorkflowLeaseRepository) Update(ctx context.Context, lease orchestrator.WorkflowLease) error { doc, err := mpostgres.NewDocument(r.workflowLeasID(lease.WorkflowID, lease.RunID), lease) if err != nil { return err @@ -60,7 +60,7 @@ func (r PostgresWorkflowLeaseRepository) Update(ctx context.Context, lease orche return nil } -func (r PostgresWorkflowLeaseRepository) Remove(ctx context.Context, workflowID string, runID string) error { +func (r WorkflowLeaseRepository) Remove(ctx context.Context, workflowID string, runID string) error { if _, err := r.collection.RemoveByID(ctx, r.workflowLeasID(workflowID, runID)); err != nil { return err } @@ -68,7 +68,7 @@ func (r PostgresWorkflowLeaseRepository) Remove(ctx context.Context, workflowID return nil } -func (r PostgresWorkflowLeaseRepository) FindByWorkflowRunID(ctx context.Context, workflowID string, runID string) (*orchestrator.WorkflowLease, error) { +func (r WorkflowLeaseRepository) FindByWorkflowRunID(ctx context.Context, workflowID string, runID string) (*orchestrator.WorkflowLease, error) { doc, err := r.collection.FindByID(ctx, r.workflowLeasID(workflowID, runID)) if err != nil { return nil, err diff --git a/internal/workflowmgmt/adapters/run_repo.go b/internal/integration/postgres/run_repo.go similarity index 96% rename from internal/workflowmgmt/adapters/run_repo.go rename to internal/integration/postgres/run_repo.go index 722040b..7710e4d 100644 --- a/internal/workflowmgmt/adapters/run_repo.go +++ b/internal/integration/postgres/run_repo.go @@ -1,12 +1,12 @@ -package adapters +package postgres import ( "context" + "github.com/morebec/smallflow/internal/business/workflowmgmt" "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/muuid" "github.com/morebec/go-misas/mx" - "github.com/morebec/smallflow/internal/workflowmgmt" "github.com/samber/lo" ) diff --git a/internal/workflowmgmt/adapters/workflow_repo.go b/internal/integration/postgres/workflow_repo.go similarity index 96% rename from internal/workflowmgmt/adapters/workflow_repo.go rename to internal/integration/postgres/workflow_repo.go index ff55858..d12077e 100644 --- a/internal/workflowmgmt/adapters/workflow_repo.go +++ b/internal/integration/postgres/workflow_repo.go @@ -1,13 +1,13 @@ -package adapters +package postgres import ( "context" "fmt" + "github.com/morebec/smallflow/internal/business/workflowmgmt" "github.com/morebec/go-misas/misas" "github.com/morebec/go-misas/muuid" "github.com/morebec/go-misas/mx" - "github.com/morebec/smallflow/internal/workflowmgmt" "github.com/samber/lo" )