Files
orbit/sdk/go/examples/worker-protocol/main.go

215 lines
6.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// worker-protocol 演示一次文本规范化任务的真实执行与原消息重报。
// 这是底层协议示例;持续运行、自动续租与优雅退出由后续 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 {
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 json.Unmarshal(raw, &credentials) != 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
}
// 每次运行使用独立能力名,避免接走前一次示例遗留的任务。
operation, err := orbit.NewWorkerMessageID()
if err != nil {
return err
}
handler := "text.normalize." + operation
registration, err := orbit.NewWorkerRegistration(queue, 1, []orbit.WorkerCapability{{Handler: handler, HandlerVersion: "v1", PayloadVersion: "v1"}})
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
accepted, err := client.SubmitTask(ctx, "normalize-"+operation, orbit.SubmitTaskRequest{QueueID: queue, Handler: handler, HandlerVersion: "v1", PayloadVersion: "v1", Payload: json.RawMessage(`{"text":" Hello Orbit "}`), RecoverySafety: orbit.RecoveryIdempotent, TaskTimeoutMS: 60000, Retry: orbit.RetryPolicy{MaxAttempts: 1, AttemptTimeoutMS: 20000}})
if err != nil {
return err
}
session := &demoSession{client: client, registration: registration}
defer session.close()
if err := session.connect(ctx); err != nil {
return err
}
capacityID, err := orbit.NewWorkerMessageID()
if err != nil {
return err
}
if _, err := session.conn.Capacity(ctx, capacityID, orbit.WorkerCapacity{Sequence: 1, Capacity: 1}); err != nil {
return err
}
assignment, err := session.conn.NextAssignment(ctx)
if err != nil {
return err
}
if assignment.Task.ID != accepted.Task.ID {
return errors.New("unexpected task in isolated example")
}
ref := assignment.Attempt()
session.ref = &ref
startID, err := orbit.NewWorkerMessageID()
if err != nil {
return err
}
started, err := session.confirm(ctx, func(c *orbit.WorkerConnection) (orbit.WorkerCommandResult, error) { return c.Start(ctx, startID, ref) })
if err != nil {
return err
}
deadline, active := started.Lease.ExecutionDeadline()
if !active {
return errors.New("start receipt does not grant current execution authority")
}
// 业务代码只拿任务输入;不接收注册秘密或尝试秘密。处理器只调用这一次。
work, stop := context.WithDeadline(ctx, deadline)
result, handlerErr := normalize(work, assignment.Task.Payload)
stop()
finish := orbit.WorkerFinish{Ref: ref, Outcome: orbit.WorkerSucceeded, Result: result}
if handlerErr != nil {
// 普通异常不能证明外部效果未发生;默认报告 UNKNOWN,保留核查路径。
finish.Outcome = orbit.WorkerUnknown
finish.ErrorCode = "HANDLER_UNCONFIRMED"
finish.Result = json.RawMessage(`{"reason":"handler did not confirm completion"}`)
}
finishID, err := orbit.NewWorkerMessageID()
if err != nil {
return err
}
confirmed, err := session.confirm(ctx, func(c *orbit.WorkerConnection) (orbit.WorkerCommandResult, error) {
return c.Finish(ctx, finishID, finish)
})
if err != nil {
return err
}
if confirmed.Receipt.Disposition != "APPLIED" {
return errors.New("result saved as evidence only; inspect authoritative task state")
}
current, err := client.GetTask(ctx, assignment.Task.ID)
if err != nil {
return err
}
if current.State != orbit.TaskSucceeded || current.AttemptCount != 1 || string(current.Result) != string(result) {
return errors.New("task did not complete as expected")
}
drainID, err := orbit.NewWorkerMessageID()
if err != nil {
return err
}
// 新连接尚未开启容量时,Sequence=2 仍是合法递增值。
if _, err := session.conn.Capacity(ctx, drainID, orbit.WorkerCapacity{Sequence: 2, Capacity: 0, Drain: true}); err != nil {
return err
}
fmt.Printf("task=%s final_state=%s attempts=%d result=%s\n", current.ID, current.State, current.AttemptCount, current.Result)
return nil
}
func normalize(ctx context.Context, payload json.RawMessage) (json.RawMessage, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
var in struct {
Text string `json:"text"`
}
if err := json.Unmarshal(payload, &in); err != nil {
return nil, err
}
return json.Marshal(struct {
Text string `json:"text"`
}{Text: strings.ToUpper(strings.TrimSpace(in.Text))})
}
type demoSession struct {
client *orbit.Client
registration orbit.WorkerRegistration
conn *orbit.WorkerConnection
ref *orbit.WorkerAttempt
}
func (s *demoSession) close() {
if s.conn != nil {
s.conn.Close()
}
}
func (s *demoSession) connect(ctx context.Context) error {
s.close()
c, err := s.client.DialWorker(ctx, s.registration)
if err != nil {
return err
}
s.conn = c
if s.ref != nil {
id, err := orbit.NewWorkerMessageID()
if err != nil {
return err
}
// 即使租约已失效,也可以重报原结果取得原回执或保存迟到证据。
if _, err := c.Resume(ctx, id, *s.ref); err != nil {
return err
}
}
return nil
}
// confirm 只重报控制操作,不再次调用业务函数;同一操作闭包固定消息 ID 与内容。
func (s *demoSession) confirm(ctx context.Context, call func(*orbit.WorkerConnection) (orbit.WorkerCommandResult, error)) (orbit.WorkerCommandResult, error) {
var out orbit.WorkerCommandResult
var err error
for attempt := 0; attempt < 3; attempt++ {
if attempt > 0 {
select {
case <-ctx.Done():
return out, ctx.Err()
case <-time.After(time.Duration(attempt) * 100 * time.Millisecond):
}
if err = s.connect(ctx); err != nil {
return out, err
}
}
out, err = call(s.conn)
if err == nil {
return out, nil
}
var control *orbit.WorkerError
retry := errors.Is(err, orbit.ErrWorkerDisconnected) || errors.Is(err, context.DeadlineExceeded) && ctx.Err() == nil || errors.As(err, &control) && control.Code == "STALE_SESSION"
if !retry {
return out, err
}
}
return out, fmt.Errorf("control result still unconfirmed after bounded replay: %w", err)
}