Files
orbit/sdk/go/examples/keyed-concurrency/main.go

170 lines
4.9 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.
// keyed-concurrency 演示跨队列资源互斥及资源端单调序号校验。
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"sync"
"time"
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
)
var errStale = errors.New("stale resource token")
// resource 仅用于本地示范。生产必须在资源存储事务内持久化最高序号和操作身份,
// 并与实际写入原子提交;Go mutex 无法保护其他进程或外部服务。
type resource struct {
mu sync.Mutex
namespace, key string
highest uint64
operations map[string]bool
}
func (r *resource) apply(namespace, key string, token uint64, operation string) error {
r.mu.Lock()
defer r.mu.Unlock()
if namespace != r.namespace || key != r.key || token == 0 || token < r.highest {
return errStale
}
r.highest = token
// 同一操作的重报不产生第二次模拟写入。
r.operations[operation] = true
return nil
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
token := os.Getenv("ORBIT_TOKEN")
if path := os.Getenv("ORBIT_CREDENTIALS_FILE"); path != "" && token == "" {
raw, err := os.ReadFile(path)
if err != nil {
return err
}
var c struct {
Token string `json:"token"`
}
if err = json.Unmarshal(raw, &c); err != nil {
return err
}
token = c.Token
}
api, err := orbit.NewClient(os.Getenv("ORBIT_URL"), orbit.Options{Token: token})
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
op, err := orbit.NewWorkerMessageID()
if err != nil {
return err
}
name := "keyed." + op
var queues []string
var tasks []orbit.Task
for i := 0; i < 2; i++ {
q, err := api.CreateQueue(ctx, fmt.Sprintf("queue-%s-%d", op, i), orbit.CreateQueueRequest{Name: fmt.Sprintf("%s.%d", name, i), MaxConcurrency: 4, RatePerSecond: 100})
if err != nil {
return err
}
queues = append(queues, q.Queue.ID)
in := orbit.SubmitTaskRequest{QueueID: q.Queue.ID, Handler: name, HandlerVersion: "v1", PayloadVersion: "v1", Payload: json.RawMessage(`{"object_ref":"objects/shared"}`), ConcurrencyKey: "object/" + op, TaskTimeoutMS: 20000, Retry: orbit.RetryPolicy{MaxAttempts: 1, AttemptTimeoutMS: 10000}}
out, err := api.SubmitTask(ctx, fmt.Sprintf("operation-%s-%d", op, i), in)
if err != nil {
return err
}
tasks = append(tasks, out.Task)
}
r := &resource{namespace: tasks[0].NamespaceID, key: tasks[0].ConcurrencyKey, operations: map[string]bool{}}
handler := orbit.WorkerHandler{Capability: orbit.WorkerCapability{Handler: name, HandlerVersion: "v1", PayloadVersion: "v1"}, Handle: func(ctx context.Context, e orbit.WorkerExecution) (json.RawMessage, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
// 本示例每条任务表示一个独立操作,Task.ID 在安全重试期间保持不变。
if err := r.apply(e.Task.NamespaceID, e.Task.ConcurrencyKey, e.ConcurrencyToken, e.Task.ID); err != nil {
return nil, &orbit.WorkerFailure{Code: "STALE_RESOURCE_TOKEN"}
}
return json.RawMessage(`{"updated":true}`), nil
}}
run, stop := context.WithCancel(ctx)
done := make(chan error, 2)
started := 0
defer func() {
stop()
for i := 0; i < started; i++ {
<-done
}
}()
for _, queue := range queues {
w, err := orbit.NewWorker(api, orbit.WorkerOptions{QueueID: queue, Capacity: 4, ShutdownTimeout: 5 * time.Second, Handlers: []orbit.WorkerHandler{handler}})
if err != nil {
return err
}
started++
go func() { done <- w.Run(run) }()
}
var low, high uint64
for _, task := range tasks {
for {
current, err := api.GetTask(ctx, task.ID)
if err != nil {
return err
}
if current.State == orbit.TaskSucceeded {
break
}
if current.State == orbit.TaskFailed || current.State == orbit.TaskCancelled || current.State == orbit.TaskNeedsReview {
return fmt.Errorf("unexpected task state: %s", current.State)
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(80 * time.Millisecond):
}
}
history, err := api.ListTaskAttempts(ctx, task.ID, orbit.HistoryPageRequest{})
if err != nil {
return err
}
if len(history.Items) != 1 || history.Items[0].ConcurrencyToken == 0 {
return errors.New("missing resource token")
}
v := history.Items[0].ConcurrencyToken
if low == 0 || v < low {
low = v
}
if v > high {
high = v
}
}
if low == high {
return errors.New("resource token reused")
}
if !errors.Is(r.apply(r.namespace, r.key, low, "late-write"), errStale) {
return errors.New("old resource token accepted")
}
r.mu.Lock()
count := len(r.operations)
r.mu.Unlock()
if count != 2 {
return errors.New("unexpected resource writes")
}
stop()
for started > 0 {
err := <-done
started--
if err != nil {
return err
}
}
fmt.Printf("queues=2 completed=2 tokens_increasing=true\nresource_writes=%d stale_write_rejected=true worker_drained=true\n", count)
return nil
}