docs: consolidate runnable examples and publish v0.1.1 guides
This commit is contained in:
252
examples/workflow/main.go
Normal file
252
examples/workflow/main.go
Normal file
@@ -0,0 +1,252 @@
|
||||
// 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):
|
||||
}
|
||||
}
|
||||
steps, err := client.GetWorkflowSteps(ctx, accepted.Instance.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verified := 0
|
||||
for _, step := range steps.Steps {
|
||||
if step.TaskID == nil {
|
||||
continue
|
||||
}
|
||||
source, err := client.GetTaskSource(ctx, *step.TaskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if source.Workflow == nil || source.Workflow.WorkflowID != accepted.Instance.ID || source.Workflow.DefinitionVersionID != accepted.Instance.DefinitionVersionID || source.Workflow.StepID != step.ID || source.Workflow.ActivationID != *step.ActivationID || source.Workflow.ActivationSequence != 1 {
|
||||
return errors.New("workflow task source mismatch")
|
||||
}
|
||||
verified++
|
||||
}
|
||||
if verified != 3 {
|
||||
return errors.New("incomplete workflow task sources")
|
||||
}
|
||||
fmt.Printf("workflow_sources_verified=%d fixed_version=%s\n", verified, accepted.Instance.Version)
|
||||
history, err := client.ListWorkflowActivations(ctx, accepted.Instance.ID, "receipt", orbit.HistoryPageRequest{Limit: 20})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(history.Items) != 1 {
|
||||
return errors.New("unexpected receipt activation history")
|
||||
}
|
||||
historical, err := client.GetWorkflowActivation(ctx, accepted.Instance.ID, "receipt", history.Items[0].ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if historical.Wait == nil || historical.Wait.WinnerSignalID == nil || *historical.Wait.WinnerSignalID != sent.Signal.ID {
|
||||
return errors.New("historical signal winner missing")
|
||||
}
|
||||
signals, err := client.ListWorkflowSignals(ctx, accepted.Instance.ID, orbit.HistoryPageRequest{Limit: 20})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(signals.Items) != 1 || signals.Items[0].State != "CONSUMED" || signals.Items[0].ID != sent.Signal.ID {
|
||||
return errors.New("signal history missing")
|
||||
}
|
||||
fmt.Println("workflow_history_verified=true signal_state=CONSUMED")
|
||||
// 时间线用于审计展示,游标只是倒序翻页位置,不作为可靠消息消费水位。
|
||||
cursor, eventCount, consumed := "", 0, false
|
||||
for {
|
||||
page, err := client.ListWorkflowEvents(ctx, accepted.Instance.ID, orbit.HistoryPageRequest{Limit: 5, Cursor: cursor})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, event := range page.Items {
|
||||
eventCount++
|
||||
if event.AggregateKind == "workflow_signal" && event.AggregateID == sent.Signal.ID && event.State == "CONSUMED" {
|
||||
detail, err := client.GetWorkflowEvent(ctx, accepted.Instance.ID, event.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var transition struct {
|
||||
From string `json:"from"`
|
||||
ConsumedWaitID string `json:"consumed_wait_id"`
|
||||
}
|
||||
if json.Unmarshal(detail.Payload, &transition) != nil || transition.From != "PENDING" || transition.ConsumedWaitID != historical.Wait.ID {
|
||||
return errors.New("signal transition evidence mismatch")
|
||||
}
|
||||
consumed = true
|
||||
}
|
||||
}
|
||||
if page.NextCursor == "" {
|
||||
break
|
||||
}
|
||||
cursor = page.NextCursor
|
||||
}
|
||||
if !consumed {
|
||||
return errors.New("consumed signal event missing")
|
||||
}
|
||||
fmt.Printf("workflow_timeline_verified=true events=%d signal_transition=PENDING->CONSUMED\n", eventCount)
|
||||
|
||||
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))})
|
||||
}
|
||||
Reference in New Issue
Block a user