feat: add standalone Worker protocol SDK and ACK-loss regression
This commit is contained in:
214
sdk/go/examples/worker-protocol/main.go
Normal file
214
sdk/go/examples/worker-protocol/main.go
Normal file
@@ -0,0 +1,214 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user