docs: consolidate runnable examples and publish v0.1.1 guides

This commit is contained in:
2026-09-24 21:27:25 +08:00
parent e29dec717f
commit 81e56bd633
124 changed files with 1430 additions and 281 deletions

252
examples/workflow/main.go Normal file
View 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))})
}