feat: add durable relative task delays and outage recovery regression
This commit is contained in:
129
sdk/go/examples/delayed-task/main.go
Normal file
129
sdk/go/examples/delayed-task/main.go
Normal file
@@ -0,0 +1,129 @@
|
||||
// delayed-task 演示数据库计时的相对延迟、原键核对和实际 Worker 执行。
|
||||
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 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
|
||||
}
|
||||
op, err := orbit.NewWorkerMessageID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
in := orbit.SubmitTaskRequest{QueueID: queue, Handler: "object.delayed." + op, HandlerVersion: "v1", PayloadVersion: "v1", Payload: json.RawMessage(`{"object_ref":"examples/input","sequence":9007199254740993}`), DelayMS: 1500, TaskTimeoutMS: 15000, Retry: orbit.RetryPolicy{MaxAttempts: 1, AttemptTimeoutMS: 5000}}
|
||||
accepted, err := client.SubmitTask(ctx, op, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 先输出任务身份,后续失败可据此核对;不输出凭据或任意业务正文。
|
||||
fmt.Printf("task=%s delay_ms=%d\n", accepted.Task.ID, in.DelayMS)
|
||||
if !accepted.Task.AvailableAt.Equal(accepted.Task.CreatedAt.Add(1500 * time.Millisecond)) {
|
||||
return errors.New("delay was not based on database acceptance")
|
||||
}
|
||||
w, err := orbit.NewWorker(client, orbit.WorkerOptions{QueueID: queue, Capacity: 1, ShutdownTimeout: 5 * time.Second, Handlers: []orbit.WorkerHandler{{Capability: orbit.WorkerCapability{Handler: in.Handler, HandlerVersion: "v1", PayloadVersion: "v1"}, Handle: func(ctx context.Context, e orbit.WorkerExecution) (json.RawMessage, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 无外部副作用的示例处理器,保持精确数字文本。
|
||||
return e.Task.Payload, nil
|
||||
}}}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
work, stop := context.WithCancel(ctx)
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- w.Run(work) }()
|
||||
waited := false
|
||||
defer func() {
|
||||
stop()
|
||||
if !waited {
|
||||
<-done
|
||||
}
|
||||
}()
|
||||
for {
|
||||
current, err := client.GetTask(ctx, accepted.Task.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current.State == orbit.TaskSucceeded {
|
||||
history, err := client.ListTaskAttempts(ctx, current.ID, orbit.HistoryPageRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(history.Items) != 1 || history.Items[0].AuthorizedAt == nil || history.Items[0].CreatedAt.Before(accepted.Task.AvailableAt) || history.Items[0].AuthorizedAt.Before(accepted.Task.AvailableAt) {
|
||||
return errors.New("execution preceded the saved release time")
|
||||
}
|
||||
break
|
||||
}
|
||||
if current.State == orbit.TaskFailed || current.State == orbit.TaskNeedsReview || current.State == orbit.TaskCancelled {
|
||||
return fmt.Errorf("task requires attention: %s", current.State)
|
||||
}
|
||||
select {
|
||||
case err := <-done:
|
||||
waited = true
|
||||
return fmt.Errorf("Worker stopped before completion: %v", err)
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
duplicate, err := client.SubmitTask(ctx, op, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !duplicate.Deduplicated || duplicate.Task.ID != accepted.Task.ID || !duplicate.Task.AvailableAt.Equal(accepted.Task.AvailableAt) {
|
||||
return errors.New("replay changed the original delayed task")
|
||||
}
|
||||
stop()
|
||||
err = <-done
|
||||
waited = true
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stats := w.Stats()
|
||||
if stats.Started != 1 || stats.ReportsConfirmed != 1 || stats.Running != 0 {
|
||||
return errors.New("Worker did not confirm and drain")
|
||||
}
|
||||
fmt.Println("state=SUCCEEDED attempts=1 no_early_start=true original_receipt=true worker_drained=true")
|
||||
return nil
|
||||
}
|
||||
@@ -52,15 +52,32 @@ func run() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
businessKey := "object /+% é " + op + " "
|
||||
businessKey := "对象 /+% é " + op + " "
|
||||
var tasks []orbit.Task
|
||||
for i, key := range []string{businessKey, businessKey, businessKey[:len(businessKey)-1]} {
|
||||
out, err := client.SubmitTask(ctx, fmt.Sprintf("query-%s-%d", op, i), orbit.SubmitTaskRequest{QueueID: queue, Handler: "query." + op, HandlerVersion: "v1", PayloadVersion: "v1", Payload: json.RawMessage(`{"object_ref":"examples/input"}`), BusinessKey: key, TaskTimeoutMS: 60000})
|
||||
payload := json.RawMessage(`{"object_ref":"examples/input","format":"text"}`)
|
||||
if i == 1 {
|
||||
payload = json.RawMessage(`{"object_ref":"examples/input","format":"json"}`)
|
||||
}
|
||||
out, err := client.SubmitTask(ctx, fmt.Sprintf("query-%s-%d", op, i), orbit.SubmitTaskRequest{QueueID: queue, Handler: "query." + op, HandlerVersion: "v1", PayloadVersion: "v1", Payload: payload, BusinessKey: key, TaskTimeoutMS: 60000})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tasks = append(tasks, out.Task)
|
||||
}
|
||||
// 改参数使用新身份与相同关联键,读取旧任务确认原输入仍保留。
|
||||
for _, task := range tasks[:2] {
|
||||
current, err := client.GetTask(ctx, task.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(current.Payload) != string(task.Payload) {
|
||||
return errors.New("new linked submission changed existing input")
|
||||
}
|
||||
}
|
||||
if tasks[0].ID == tasks[1].ID || string(tasks[0].Payload) == string(tasks[1].Payload) {
|
||||
return errors.New("parameter change did not create an independent task")
|
||||
}
|
||||
from, before := tasks[0].CreatedAt, tasks[2].CreatedAt.Add(time.Microsecond)
|
||||
query := orbit.ListTasksRequest{QueueID: queue, State: orbit.TaskWaiting, BusinessKey: businessKey, CreatedFrom: &from, CreatedBefore: &before, Limit: 1}
|
||||
matched, pages := 0, 0
|
||||
@@ -99,6 +116,6 @@ func run() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
fmt.Printf("matched=%d pages=%d before_excluded=true cancelled=%d\n", matched, pages, len(tasks))
|
||||
fmt.Printf("matched=%d pages=%d before_excluded=true original_payload_preserved=true cancelled=%d\n", matched, pages, len(tasks))
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user