feat: add durable business task outbox example and fault recovery
This commit is contained in:
192
examples/task-outbox/cmd/sender/main.go
Normal file
192
examples/task-outbox/cmd/sender/main.go
Normal file
@@ -0,0 +1,192 @@
|
||||
// sender 演示业务事务Outbox与可重启的有限发送器。
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/orbit/examples/task-outbox/internal/outbox"
|
||||
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 {
|
||||
if len(os.Args) < 2 {
|
||||
return errors.New("usage: sender init|enqueue <operation_id> <source_ref>|send|status <operation_id>|demo")
|
||||
}
|
||||
command := os.Args[1]
|
||||
if !(command == "init" && len(os.Args) == 2 || command == "send" && len(os.Args) == 2 || command == "demo" && len(os.Args) == 2 || command == "enqueue" && len(os.Args) == 4 || command == "status" && len(os.Args) == 3) {
|
||||
return errors.New("invalid sender command or arguments")
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
open, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
db, err := outbox.Open(open, os.Getenv("ORBIT_SENDER_DSN"))
|
||||
cancel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
if command == "init" {
|
||||
bounded, done := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer done()
|
||||
if err = outbox.Init(bounded, db); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("sender_schema_initialized=true foreign_keys=0")
|
||||
return nil
|
||||
}
|
||||
api, ns, queue, err := client()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store, err := outbox.New(db, ns, queue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch command {
|
||||
case "enqueue":
|
||||
id, source := os.Args[2], os.Args[3]
|
||||
request := request(queue, id, source)
|
||||
duplicate, err := store.Enqueue(ctx, id, source, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("operation=%s local_committed=true duplicate=%t\n", id, duplicate)
|
||||
return nil
|
||||
case "status":
|
||||
r, err := store.Status(ctx, os.Args[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printRecord(r)
|
||||
return nil
|
||||
case "demo":
|
||||
return demo(ctx, store, api, queue)
|
||||
case "send":
|
||||
for ctx.Err() == nil {
|
||||
r, err := store.SendOnce(ctx, api)
|
||||
wait := 500 * time.Millisecond
|
||||
if err == nil {
|
||||
printRecord(r)
|
||||
} else if !errors.Is(err, outbox.ErrIdle) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
wait = 2 * time.Second
|
||||
}
|
||||
timer := time.NewTimer(wait)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func client() (*orbit.Client, string, string, error) {
|
||||
token, ns, queue := os.Getenv("ORBIT_TOKEN"), os.Getenv("ORBIT_NAMESPACE_ID"), os.Getenv("ORBIT_QUEUE_ID")
|
||||
if path := os.Getenv("ORBIT_CREDENTIALS_FILE"); path != "" {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, "", "", errors.New("cannot read sender credential file")
|
||||
}
|
||||
var c struct {
|
||||
Token string `json:"token"`
|
||||
NamespaceID string `json:"namespace_id"`
|
||||
QueueID string `json:"queue_id"`
|
||||
}
|
||||
if json.Unmarshal(raw, &c) != nil {
|
||||
return nil, "", "", errors.New("invalid sender credential file")
|
||||
}
|
||||
if token == "" {
|
||||
token = c.Token
|
||||
}
|
||||
if ns == "" {
|
||||
ns = c.NamespaceID
|
||||
}
|
||||
if queue == "" {
|
||||
queue = c.QueueID
|
||||
}
|
||||
}
|
||||
if token == "" {
|
||||
return nil, "", "", errors.New("sender credential is required")
|
||||
}
|
||||
c, err := orbit.NewClient(os.Getenv("ORBIT_URL"), orbit.Options{Token: token})
|
||||
return c, ns, queue, err
|
||||
}
|
||||
func request(queue, id, source string) orbit.SubmitTaskRequest {
|
||||
payload, _ := json.Marshal(struct {
|
||||
SourceRef string `json:"source_ref"`
|
||||
Sequence uint64 `json:"sequence"`
|
||||
}{source, 9007199254740993})
|
||||
return orbit.SubmitTaskRequest{QueueID: queue, Handler: "object.prepare", HandlerVersion: "v1", PayloadVersion: "v1", Payload: payload, BusinessKey: id, TaskTimeoutMS: 60000}
|
||||
}
|
||||
func printRecord(r outbox.Record) {
|
||||
task := ""
|
||||
if r.TaskID != nil {
|
||||
task = *r.TaskID
|
||||
}
|
||||
fmt.Printf("operation=%s state=%s sends=%d code=%s task=%s\n", r.OperationID, r.State, r.SendCount, r.LastCode, task)
|
||||
}
|
||||
func demo(parent context.Context, s *outbox.Store, api *orbit.Client, queue string) error {
|
||||
ctx, stop := context.WithTimeout(parent, 20*time.Second)
|
||||
defer stop()
|
||||
id, err := orbit.NewEventMessageID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 演示每次新建一个操作;真实接入必须先由业务方保存ID再调用Enqueue。
|
||||
fmt.Printf("demo_operation=%s\n", id)
|
||||
in := request(queue, id, "objects/outbox-demo.txt")
|
||||
if _, err = s.Enqueue(ctx, id, "objects/outbox-demo.txt", in); err != nil {
|
||||
return err
|
||||
}
|
||||
duplicate, err := s.Enqueue(ctx, id, "objects/outbox-demo.txt", in)
|
||||
if err != nil || !duplicate {
|
||||
return errors.New("outbox local duplicate was not recovered")
|
||||
}
|
||||
for {
|
||||
r, err := s.Status(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.State == "REVIEW" {
|
||||
return errors.New("outbox demo needs review using its printed operation identity")
|
||||
}
|
||||
if r.State == "ACCEPTED" {
|
||||
task, err := api.GetTask(ctx, *r.TaskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = api.CancelTask(ctx, task.ID, "outbox-demo-cancel-"+id, orbit.CancelTaskRequest{ExpectedRevision: task.Revision, Reason: "outbox submission demo completed"}); err != nil {
|
||||
return err
|
||||
}
|
||||
printRecord(r)
|
||||
fmt.Println("local_duplicate=true accepted_once=true execution_success_not_implied=true")
|
||||
return nil
|
||||
}
|
||||
_, err = s.SendOnce(ctx, api)
|
||||
if err != nil && !errors.Is(err, outbox.ErrIdle) {
|
||||
return err
|
||||
}
|
||||
timer := time.NewTimer(100 * time.Millisecond)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user