211 lines
6.3 KiB
Go
211 lines
6.3 KiB
Go
// receiver 使用独立业务数据库演示结果Inbox与版本化业务视图。
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"git.yuntuo.hk/k2safe/orbit/examples/result-inbox/internal/inbox"
|
|
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 || (os.Args[1] != "init" && os.Args[1] != "consume" && os.Args[1] != "demo") {
|
|
return errors.New("usage: receiver init|consume|demo")
|
|
}
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
open, cancel := context.WithTimeout(ctx, 10*time.Second)
|
|
db, err := inbox.Open(open, os.Getenv("ORBIT_RECEIVER_DSN"))
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
if os.Args[1] == "init" {
|
|
bounded, stop := context.WithTimeout(ctx, 10*time.Second)
|
|
defer stop()
|
|
if err = inbox.Init(bounded, db); err != nil {
|
|
return err
|
|
}
|
|
fmt.Println("receiver_schema_initialized=true foreign_keys=0")
|
|
return nil
|
|
}
|
|
api, queue, err := client()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name := os.Getenv("RECEIVER_NAME")
|
|
if name == "" {
|
|
name = "task-result-view"
|
|
}
|
|
store, err := inbox.New(db, name, queue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if os.Args[1] == "demo" {
|
|
return demo(ctx, api, store, queue)
|
|
}
|
|
check, checkStop := context.WithTimeout(ctx, 5*time.Second)
|
|
subscription, err := api.GetSubscription(check, os.Getenv("ORBIT_SUBSCRIPTION_ID"))
|
|
checkStop()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if subscription.QueueID != queue {
|
|
return errors.New("subscription does not match configured receiver queue")
|
|
}
|
|
consumer, err := orbit.NewEventConsumer(api, orbit.EventConsumerOptions{SubscriptionID: subscription.ID, Capacity: 4, Handle: func(work context.Context, event orbit.ResultEvent) error {
|
|
err := store.Handle(work, event)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "receiver_event=%s error=%v\n", event.ID, err)
|
|
}
|
|
return err
|
|
}, ShutdownTimeout: 10 * time.Second})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
observed := make(chan struct{})
|
|
go func() {
|
|
defer close(observed)
|
|
for event := range consumer.Events() {
|
|
fmt.Printf("consumer_event=%s event_id=%s code=%s\n", event.Type, event.EventID, event.Code)
|
|
}
|
|
}()
|
|
err = consumer.Run(ctx)
|
|
<-observed
|
|
return err
|
|
}
|
|
func client() (*orbit.Client, string, 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 nil, "", errors.New("cannot read credential file")
|
|
}
|
|
var c struct {
|
|
Token string `json:"token"`
|
|
QueueID string `json:"queue_id"`
|
|
}
|
|
if json.Unmarshal(raw, &c) != nil {
|
|
return nil, "", errors.New("invalid credential file")
|
|
}
|
|
if token == "" {
|
|
token = c.Token
|
|
}
|
|
if queue == "" {
|
|
queue = c.QueueID
|
|
}
|
|
}
|
|
if token == "" || queue == "" {
|
|
return nil, "", errors.New("event-enabled credential and queue are required")
|
|
}
|
|
c, err := orbit.NewClient(os.Getenv("ORBIT_URL"), orbit.Options{Token: token})
|
|
return c, queue, err
|
|
}
|
|
func demo(parent context.Context, api *orbit.Client, store *inbox.Store, queue string) error {
|
|
ctx, stop := context.WithTimeout(parent, 20*time.Second)
|
|
defer stop()
|
|
id, err := orbit.NewEventMessageID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
created, err := api.CreateSubscription(ctx, "inbox-sub-"+id, orbit.CreateSubscriptionRequest{Name: "inbox-" + id, QueueID: queue, Types: []string{"task.cancelled"}, MaxPending: 10, MaxAttempts: 3})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
task, err := api.SubmitTask(ctx, "inbox-task-"+id, orbit.SubmitTaskRequest{QueueID: queue, Handler: "object.prepare", HandlerVersion: "v1", PayloadVersion: "v1", Payload: json.RawMessage(`{}`)})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err = api.CancelTask(ctx, task.Task.ID, "inbox-cancel-"+id, orbit.CancelTaskRequest{ExpectedRevision: task.Task.Revision, Reason: "durable inbox example"}); err != nil {
|
|
return err
|
|
}
|
|
duplicate := make(chan bool, 1)
|
|
consumer, err := orbit.NewEventConsumer(api, orbit.EventConsumerOptions{SubscriptionID: created.Subscription.ID, Capacity: 1, ShutdownTimeout: 5 * time.Second, Handle: func(work context.Context, event orbit.ResultEvent) error {
|
|
if event.Snapshot.TaskID != task.Task.ID {
|
|
return inbox.ErrInvalidEvent
|
|
}
|
|
if _, err := store.Apply(work, event); err != nil {
|
|
return err
|
|
}
|
|
// 再用同一冻结事件进入业务事务,证明重复不会再次推进视图;真实进程故障另有回归。
|
|
again, err := store.Apply(work, event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !again.Duplicate {
|
|
return errors.New("receiver duplicate was not recognized")
|
|
}
|
|
select {
|
|
case duplicate <- true:
|
|
default:
|
|
}
|
|
return nil
|
|
}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
run, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
done := make(chan error, 1)
|
|
go func() { done <- consumer.Run(run) }()
|
|
confirmed := false
|
|
for !confirmed {
|
|
select {
|
|
case <-ctx.Done():
|
|
cancel()
|
|
<-done
|
|
return errors.New("inbox demo timed out before confirmed acknowledgement")
|
|
case err := <-done:
|
|
if err == nil {
|
|
err = errors.New("consumer stopped before acknowledgement")
|
|
}
|
|
return err
|
|
case event, ok := <-consumer.Events():
|
|
if !ok {
|
|
if err := <-done; err != nil {
|
|
return err
|
|
}
|
|
return errors.New("consumer stopped before acknowledgement")
|
|
}
|
|
confirmed = event.Type == "acknowledged"
|
|
}
|
|
}
|
|
cancel()
|
|
if err = <-done; err != nil {
|
|
return err
|
|
}
|
|
select {
|
|
case <-duplicate:
|
|
default:
|
|
return errors.New("duplicate verification missing")
|
|
}
|
|
view, err := store.View(ctx, task.Task.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if view.AppliedEvents != 1 || view.LastState != "CANCELLED" {
|
|
return errors.New("duplicate changed business projection")
|
|
}
|
|
paused, err := api.ConfigureSubscription(ctx, created.Subscription.ID, "inbox-pause-"+id, orbit.ConfigureSubscriptionRequest{ExpectedRevision: created.Subscription.Revision, Enabled: false, Reason: "durable inbox example completed"})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("task=%s state=%s inbox_duplicate=true applied_events=%d\n", task.Task.ID, view.LastState, view.AppliedEvents)
|
|
fmt.Printf("ack_confirmed=%d pending=%d subscription_enabled=%t\n", consumer.Stats().Acknowledged, paused.Subscription.PendingCount, paused.Subscription.Enabled)
|
|
return nil
|
|
}
|