feat: expose workflow runtime through HTTP and Go SDK

This commit is contained in:
2026-09-18 06:51:08 +08:00
parent af1ae4cb21
commit b9e4191c6b
50 changed files with 2221 additions and 143 deletions

View File

@@ -0,0 +1,174 @@
// workflow 通过公共SDK运行并行处理、计时等待、外部回执与后续发布;业务处理器在Worker中执行。
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"time"
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
)
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
raw, err := os.ReadFile(os.Getenv("ORBIT_CREDENTIALS_FILE"))
if err != nil {
return err
}
var credentials struct {
Token string `json:"token"`
QueueID string `json:"queue_id"`
}
if json.Unmarshal(raw, &credentials) != nil || credentials.Token == "" || credentials.QueueID == "" {
return errors.New("invalid private credentials file")
}
client, err := orbit.NewClient(os.Getenv("ORBIT_URL"), orbit.Options{Token: credentials.Token})
if err != nil {
return err
}
operation, err := orbit.NewWorkerMessageID()
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
handler := "artifact.normalize." + operation
typ := orbit.WorkflowType{Kind: "OBJECT", Fields: map[string]orbit.WorkflowType{"text": {Kind: "STRING"}}}
task := orbit.WorkflowTask{QueueID: credentials.QueueID, Handler: handler, HandlerVersion: "v1", PayloadVersion: "v1", InputType: typ, Input: orbit.WorkflowRef("input"), ResultType: typ, TimeoutMS: 20000, Retry: orbit.RetryPolicy{MaxAttempts: 1, AttemptTimeoutMS: 5000}, RecoverySafety: orbit.RecoveryIdempotent}
pair := orbit.WorkflowParallel{Branches: []orbit.WorkflowParallelBranch{
{Name: "left", Body: orbit.WorkflowBlock{Steps: []orbit.WorkflowStep{{Key: "left", Kind: "TASK", Task: &task}}, OutputType: typ, Output: orbit.WorkflowRef("left")}},
{Name: "right", Body: orbit.WorkflowBlock{Steps: []orbit.WorkflowStep{{Key: "right", Kind: "TASK", Task: &task}}, OutputType: typ, Output: orbit.WorkflowRef("right")}},
}}
publish := task
publish.Input = orbit.WorkflowRef("pair", "left")
spec := orbit.WorkflowSpecification{SchemaVersion: 1, TimeoutMS: 60000, FailurePolicy: "REVIEW", InputType: typ, Body: orbit.WorkflowBlock{Steps: []orbit.WorkflowStep{
{Key: "pair", Kind: "PARALLEL", Parallel: &pair},
{Key: "cooldown", Kind: "TIMER", Timer: &orbit.WorkflowTimer{DelayMS: 500}},
{Key: "receipt", Kind: "SIGNAL", Signal: &orbit.WorkflowSignal{Name: "artifact.confirmed", PayloadVersion: "v1", Correlation: orbit.WorkflowValue{Kind: "LITERAL", Literal: json.RawMessage(`"` + operation + `"`)}, PayloadType: orbit.WorkflowType{Kind: "BOOLEAN"}, TimeoutMS: 30000}},
{Key: "publish", Kind: "TASK", Task: &publish},
}, OutputType: typ, Output: orbit.WorkflowRef("publish")}}
definition, err := client.CreateWorkflowDefinition(ctx, operation, orbit.CreateWorkflowDefinitionRequest{Name: "artifact." + operation, Description: "并行结果生成与模拟外部回执"})
if err != nil {
return err
}
if _, err = client.PublishWorkflowVersion(ctx, definition.Definition.ID, operation, orbit.PublishWorkflowVersionRequest{Version: "v1", Specification: spec, Reason: "publish runnable workflow example"}); err != nil {
return err
}
start := orbit.StartWorkflowRequest{DefinitionID: definition.Definition.ID, Version: "v1", Input: json.RawMessage(`{"text":" hello orbit "}`), BusinessKey: operation}
accepted, err := client.StartWorkflow(ctx, operation, start)
if err != nil {
return err
}
repeated, err := client.StartWorkflow(ctx, operation, start)
if err != nil || !repeated.Deduplicated || repeated.Instance.ID != accepted.Instance.ID {
return errors.New("start identity was not preserved")
}
worker, err := orbit.NewWorker(client, orbit.WorkerOptions{QueueID: credentials.QueueID, Capacity: 2, ShutdownTimeout: 5 * time.Second, Handlers: []orbit.WorkerHandler{{Capability: orbit.WorkerCapability{Handler: handler, HandlerVersion: "v1", PayloadVersion: "v1"}, Handle: normalize}}})
if err != nil {
return err
}
workerCtx, stop := context.WithCancel(ctx)
done := make(chan error, 1)
go func() { done <- worker.Run(workerCtx) }()
waited := false
defer func() {
stop()
if !waited {
<-done
}
}()
var wait orbit.WorkflowStepDetail
for {
wait, err = client.GetWorkflowStep(ctx, accepted.Instance.ID, "receipt")
if err != nil {
return err
}
if wait.Step.State == "WAITING" {
break
}
current, e := client.GetWorkflow(ctx, accepted.Instance.ID)
if e != nil {
return e
}
if current.State == "NEEDS_REVIEW" || current.State == "FAILED" {
return fmt.Errorf("workflow requires attention: %s", current.State)
}
select {
case e := <-done:
waited = true
return fmt.Errorf("Worker stopped: %v", e)
case <-ctx.Done():
return ctx.Err()
case <-time.After(100 * time.Millisecond):
}
}
if wait.Wait == nil || wait.Step.ActivationSequence == 0 {
return errors.New("wait contract missing")
}
// 示例模拟外部系统的已验证回执。实际系统由独立、具备信号权限的调用者确认业务事实。
signal := orbit.SendWorkflowSignalRequest{Name: *wait.Wait.Name, PayloadVersion: *wait.Wait.PayloadVersion, TargetStepKey: wait.Step.Key, TargetActivationSequence: wait.Step.ActivationSequence, CorrelationKey: wait.Wait.CorrelationKey, Payload: json.RawMessage(`true`), Reason: "simulated verified external receipt"}
sent, err := client.SendWorkflowSignal(ctx, accepted.Instance.ID, operation, signal)
if err != nil {
return err
}
again, err := client.SendWorkflowSignal(ctx, accepted.Instance.ID, operation, signal)
if err != nil || !again.Deduplicated || again.Signal.ID != sent.Signal.ID {
return errors.New("signal identity was not preserved")
}
for {
current, e := client.GetWorkflow(ctx, accepted.Instance.ID)
if e != nil {
return e
}
if current.State == "SUCCEEDED" {
fmt.Printf("workflow=%s state=%s result=%s start_deduplicated=%t signal_deduplicated=%t\n", current.ID, current.State, current.Output, repeated.Deduplicated, again.Deduplicated)
break
}
if current.State == "NEEDS_REVIEW" || current.State == "FAILED" || current.State == "CANCELLED" {
return fmt.Errorf("workflow requires attention: %s", current.State)
}
select {
case e := <-done:
waited = true
return fmt.Errorf("Worker stopped: %v", e)
case <-ctx.Done():
return ctx.Err()
case <-time.After(100 * time.Millisecond):
}
}
stop()
err = <-done
waited = true
if err != nil {
return err
}
stats := worker.Stats()
if stats.Started != 3 || stats.ReportsConfirmed != 3 || stats.Running != 0 {
return errors.New("unexpected task count or incomplete drain")
}
fmt.Printf("worker_drained=true started=%d confirmed=%d\n", stats.Started, stats.ReportsConfirmed)
return nil
}
func normalize(ctx context.Context, execution orbit.WorkerExecution) (json.RawMessage, error) {
var in struct {
Text string `json:"text"`
}
if json.Unmarshal(execution.Task.Payload, &in) != nil {
return nil, &orbit.WorkerFailure{Code: "INVALID_TEXT"}
}
if err := ctx.Err(); err != nil {
return nil, err
}
return json.Marshal(struct {
Text string `json:"text"`
}{strings.ToUpper(strings.TrimSpace(in.Text))})
}