Files
orbit/examples/task-outbox/cmd/sender/main.go

193 lines
5.4 KiB
Go
Raw Permalink 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.
// 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:
}
}
}