215 lines
6.6 KiB
Go
215 lines
6.6 KiB
Go
// worker-protocol 演示一次文本规范化任务的真实执行与原消息重报。
|
||
// 这是底层协议示例;持续运行、自动续租与优雅退出见 examples/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)
|
||
}
|