feat: add audited task review and linked manual retries
This commit is contained in:
182
sdk/go/examples/recovery/main.go
Normal file
182
sdk/go/examples/recovery/main.go
Normal file
@@ -0,0 +1,182 @@
|
||||
// recovery 演示未知结果、人工核查、关联新任务和不可变审计;只处理本地模拟输入。
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"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 c struct {
|
||||
Token string `json:"token"`
|
||||
QueueID string `json:"queue_id"`
|
||||
}
|
||||
if json.Unmarshal(raw, &c) != nil {
|
||||
return errors.New("invalid credentials file")
|
||||
}
|
||||
if token == "" {
|
||||
token = c.Token
|
||||
}
|
||||
if queue == "" {
|
||||
queue = c.QueueID
|
||||
}
|
||||
}
|
||||
client, err := orbit.NewClient(os.Getenv("ORBIT_URL"), orbit.Options{Token: token})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
op, err := orbit.NewWorkerMessageID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handler := "recovery.inspect." + op
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
accepted, err := client.SubmitTask(ctx, "recovery-"+op, orbit.SubmitTaskRequest{QueueID: queue, Handler: handler, HandlerVersion: "v1", PayloadVersion: "v1", Payload: json.RawMessage(`{"object_ref":"examples/input"}`), TaskTimeoutMS: 60000, Retry: orbit.RetryPolicy{MaxAttempts: 1, AttemptTimeoutMS: 10000}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
registration, err := orbit.NewWorkerRegistration(queue, 1, []orbit.WorkerCapability{{Handler: handler, HandlerVersion: "v1", PayloadVersion: "v1"}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w, err := client.DialWorker(ctx, registration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Close()
|
||||
nextID := func() (string, error) { return orbit.NewWorkerMessageID() }
|
||||
id, err := nextID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Capacity(ctx, id, orbit.WorkerCapacity{Sequence: 1, Capacity: 1}); err != nil {
|
||||
return err
|
||||
}
|
||||
a, err := w.NextAssignment(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if a.Task.ID != accepted.Task.ID {
|
||||
return errors.New("unexpected task")
|
||||
}
|
||||
id, err = nextID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
started, err := w.Start(ctx, id, a.Attempt())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, active := started.Lease.ExecutionDeadline(); !active {
|
||||
return errors.New("missing current execution authority")
|
||||
}
|
||||
// 本例不操作外部系统:模拟“旧执行已结束,但最初无法判断结果”的场景。
|
||||
id, err = nextID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Finish(ctx, id, orbit.WorkerFinish{Ref: a.Attempt(), Outcome: orbit.WorkerUnknown, Result: json.RawMessage(`{"status":"unknown"}`), ErrorCode: "EXAMPLE_UNCERTAIN"}); err != nil {
|
||||
return err
|
||||
}
|
||||
current, err := client.GetTask(ctx, a.Task.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current.State != orbit.TaskNeedsReview {
|
||||
return errors.New("expected manual review")
|
||||
}
|
||||
// 生产中先核查旧执行和外部效果,保存真实证据,再由专门管理凭据提交此结论。
|
||||
// 每次操作将幂等键与完整请求持久化;响应丢失时保持原键、版本、结论与证据重报。
|
||||
reviewKey := "review-" + op
|
||||
reviewInput := orbit.ReviewTaskRequest{ExpectedRevision: current.Revision, Reason: "模拟旧执行已结束,确认操作未完成", EvidenceRef: "evidence:example-reconciliation", ExecutionSettled: true, Outcome: orbit.TaskFailed, Result: json.RawMessage(`{"confirmed":true,"sequence":9007199254740993}`)}
|
||||
reviewed, err := client.ReviewTask(ctx, current.ID, reviewKey, reviewInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
replay, err := client.ReviewTask(ctx, current.ID, reviewKey, reviewInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !replay.Deduplicated || replay.AuditID != reviewed.AuditID {
|
||||
return errors.New("review identity changed")
|
||||
}
|
||||
// 人工重试保留原任务,另建同业务身份的新任务,并显式给出新的总时间预算。
|
||||
retryKey := "retry-" + op
|
||||
retryInput := orbit.RetryTaskRequest{ExpectedRevision: reviewed.Task.Revision, Reason: "已核实模拟任务可安全重新执行", EvidenceRef: "evidence:example-safe-retry", SafeToRetry: true, TaskTimeoutMS: 60000}
|
||||
retried, err := client.RetryTask(ctx, current.ID, retryKey, retryInput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a, err = w.NextAssignment(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if a.Task.ID != retried.Task.ID || a.Task.RetryOf == nil || *a.Task.RetryOf != current.ID {
|
||||
return errors.New("missing retry lineage")
|
||||
}
|
||||
id, err = nextID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
started, err = w.Start(ctx, id, a.Attempt())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, active := started.Lease.ExecutionDeadline(); !active {
|
||||
return errors.New("missing retry execution authority")
|
||||
}
|
||||
id, err = nextID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Finish(ctx, id, orbit.WorkerFinish{Ref: a.Attempt(), Outcome: orbit.WorkerSucceeded, Result: json.RawMessage(`{"object_ref":"examples/output"}`)}); err != nil {
|
||||
return err
|
||||
}
|
||||
audit, err := client.GetTaskOperation(ctx, current.ID, retried.AuditID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
page, err := client.ListTaskOperations(ctx, current.ID, orbit.HistoryPageRequest{Limit: 20})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
original, err := client.GetTask(ctx, current.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
child, err := client.GetTask(ctx, retried.Task.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if original.State != orbit.TaskFailed || child.State != orbit.TaskSucceeded || audit.Action != "task.retry" {
|
||||
return errors.New("unexpected recovery result")
|
||||
}
|
||||
id, err = nextID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Capacity(ctx, id, orbit.WorkerCapacity{Sequence: 2, Drain: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("source=%s state=%s retry=%s state=%s audit=%s operations=%d\n", original.ID, original.State, child.ID, child.State, audit.ID, len(page.Items))
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user