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

View File

@@ -0,0 +1,30 @@
# CSV 转 JSON
提交小型 CSV 内容,Worker 转为 JSON,并核对行数和输出。
## 准备
- Go 1.27.1、当前版本 Orbit;服务启动和初始化见[仓库 README](../../README.md#快速开始)。
- 初始化时增加 --worker-access;输入内置,不需要外部文件存储。
- [示例总索引](../README.md)提供统一初始化和权限说明;完整字段、失败处理见[详细手册](../../docs/examples/file-conversion.md)。
## 运行
以下命令从仓库根目录执行;凭据路径替换为实际初始化输出。
```sh
cd examples
export ORBIT_URL=http://127.0.0.1:8080
export ORBIT_CREDENTIALS_FILE="$PWD/../.local/development.credentials.json"
go run ./file-conversion
```
## 结果核对
rows=2 state=SUCCEEDED worker_drained=true。具体 ID 和统计值以本次运行为准。程序会核对上述关键条件,失败时打印错误并以非零状态退出。
## 接入自己的项目
阅读[main.go](main.go)中的客户端创建、请求及错误处理。业务项目通过 `go get git.yuntuo.hk/k2safe/orbit/sdk/go@v0.1.1` 引入 SDK,无需导入本示例包。
示例运行会写入真实平台数据,应使用专用测试应用/队列。正常结束保留历史和审计;重复演示通常创建新身份,不能当作上次失败操作的恢复。真实业务应在第一次请求前持久化操作身份,网络结果不明时沿原身份核对。

View File

@@ -0,0 +1,218 @@
// file-conversion 演示一个通用的文件格式转换任务:CSV 内容由 Worker 转为 JSON。
// 示例把小文件内容放进任务快照;生产大文件应传对象引用,并在业务侧保存稳定操作键。
package main
import (
"context"
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strings"
"time"
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
)
type conversionInput struct {
// FileName 是输入文件名,仅用于结果追踪,不参与路径访问。
FileName string `json:"file_name"`
// Content 是示例中的小型文件正文;生产场景应替换为受控对象引用。
Content string `json:"content"`
// TargetFormat 固定为 json,演示处理器版本化能力。
TargetFormat string `json:"target_format"`
}
type conversionOutput struct {
// FileName 是转换后文件名。
FileName string `json:"file_name"`
// Format 是输出格式。
Format string `json:"format"`
// Content 是转换后的 JSON 文件正文。
Content string `json:"content"`
// Rows 是转换的记录数。
Rows int `json:"rows"`
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
token, queue := os.Getenv("ORBIT_TOKEN"), os.Getenv("ORBIT_QUEUE_ID")
if path := os.Getenv("ORBIT_CREDENTIALS_FILE"); path != "" {
raw, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read private credentials: %w", err)
}
var credentials struct {
Token string `json:"token"`
QueueID string `json:"queue_id"`
}
if err := json.Unmarshal(raw, &credentials); err != nil {
return errors.New("invalid credentials file")
}
if token == "" {
token = credentials.Token
}
if queue == "" {
queue = credentials.QueueID
}
}
client, err := orbit.NewClient(os.Getenv("ORBIT_URL"), orbit.Options{Token: token})
if err != nil {
return err
}
runID, err := orbit.NewWorkerMessageID()
if err != nil {
return err
}
handler := "file.convert.csv-json." + runID
worker, err := orbit.NewWorker(client, orbit.WorkerOptions{
QueueID: queue,
Capacity: 1,
ShutdownTimeout: 5 * time.Second,
Handlers: []orbit.WorkerHandler{{
Capability: orbit.WorkerCapability{Handler: handler, HandlerVersion: "v1", PayloadVersion: "v1"},
Handle: convertCSV,
}},
})
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
payload, err := json.Marshal(conversionInput{
FileName: "records.csv",
Content: "name,amount\nalpha,2\nbeta,3\n",
TargetFormat: "json",
})
if err != nil {
return err
}
key := "file-conversion-" + runID
accepted, err := client.SubmitTask(ctx, key, orbit.SubmitTaskRequest{
QueueID: queue,
Handler: handler,
HandlerVersion: "v1",
PayloadVersion: "v1",
Payload: payload,
TaskTimeoutMS: 60000,
Retry: orbit.RetryPolicy{MaxAttempts: 1, AttemptTimeoutMS: 10000},
RecoverySafety: orbit.RecoveryReconcileOnly,
})
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
}
}()
for {
current, err := client.GetTask(ctx, accepted.Task.ID)
if err != nil {
return err
}
switch current.State {
case orbit.TaskSucceeded:
var output conversionOutput
if err := json.Unmarshal(current.Result, &output); err != nil {
return fmt.Errorf("decode conversion result: %w", err)
}
if output.FileName != "records.json" || output.Format != "json" || output.Rows != 2 || output.Content != `[{"amount":"2","name":"alpha"},{"amount":"3","name":"beta"}]` {
return fmt.Errorf("unexpected conversion result: %+v", output)
}
stop()
err = <-done
waited = true
if err != nil {
return err
}
stats := worker.Stats()
if stats.Started != 1 || stats.ReportsConfirmed != 1 || stats.Running != 0 {
return fmt.Errorf("unexpected Worker completion counts: %+v", stats)
}
fmt.Printf("input=%s output=%s rows=%d state=%s worker_drained=true\n", "records.csv", output.FileName, output.Rows, current.State)
return nil
case orbit.TaskFailed, orbit.TaskNeedsReview:
return fmt.Errorf("conversion task requires attention: %s", current.State)
}
select {
case err := <-done:
waited = true
return fmt.Errorf("Worker stopped before conversion completed: %v", err)
case <-ctx.Done():
return ctx.Err()
case <-time.After(100 * time.Millisecond):
}
}
}
func convertCSV(ctx context.Context, execution orbit.WorkerExecution) (json.RawMessage, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
var in conversionInput
if err := json.Unmarshal(execution.Task.Payload, &in); err != nil {
return nil, &orbit.WorkerFailure{Code: "INVALID_FILE"}
}
out, err := convert(in)
if err != nil {
return nil, &orbit.WorkerFailure{Code: "INVALID_FILE"}
}
return json.Marshal(out)
}
func convert(in conversionInput) (conversionOutput, error) {
var out conversionOutput
if strings.TrimSpace(in.FileName) == "" || in.TargetFormat != "json" || in.Content == "" {
return out, errors.New("invalid conversion input")
}
reader := csv.NewReader(strings.NewReader(in.Content))
header, err := reader.Read()
if err != nil || len(header) == 0 {
return out, errors.New("CSV header is required")
}
seen := make(map[string]struct{}, len(header))
for i := range header {
header[i] = strings.TrimSpace(header[i])
if header[i] == "" {
return out, errors.New("CSV header is empty")
}
if _, ok := seen[header[i]]; ok {
return out, errors.New("CSV header is duplicated")
}
seen[header[i]] = struct{}{}
}
rows := make([]map[string]string, 0, 16)
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil || len(record) != len(header) {
return out, errors.New("CSV row does not match header")
}
row := make(map[string]string, len(header))
for i, value := range record {
row[header[i]] = value
}
rows = append(rows, row)
}
encoded, err := json.Marshal(rows)
if err != nil {
return out, err
}
return conversionOutput{FileName: strings.TrimSuffix(in.FileName, ".csv") + ".json", Format: "json", Content: string(encoded), Rows: len(rows)}, nil
}

View File

@@ -0,0 +1,25 @@
package main
import "testing"
func TestConvertCSV(t *testing.T) {
out, err := convert(conversionInput{FileName: "records.csv", Content: "name,amount\nalpha,2\nbeta,3\n", TargetFormat: "json"})
if err != nil {
t.Fatal(err)
}
if out.FileName != "records.json" || out.Format != "json" || out.Rows != 2 || out.Content != `[{"amount":"2","name":"alpha"},{"amount":"3","name":"beta"}]` {
t.Fatalf("unexpected output: %+v", out)
}
}
func TestConvertRejectsMalformedCSV(t *testing.T) {
for _, in := range []conversionInput{
{FileName: "records.csv", Content: "name,amount\nalpha\n", TargetFormat: "json"},
{FileName: "records.csv", Content: "name,name\nalpha,beta\n", TargetFormat: "json"},
{FileName: "records.csv", Content: "name,amount\nalpha,2\n", TargetFormat: "xml"},
} {
if _, err := convert(in); err == nil {
t.Fatalf("malformed input accepted: %+v", in)
}
}
}