96 lines
2.9 KiB
Go
96 lines
2.9 KiB
Go
// Package outbox 演示在业务事务中使用公共 MySQL Outbox 适配包。
|
|
package outbox
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
_ "embed"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
adapter "git.yuntuo.hk/k2safe/orbit/adapters/outbox/mysql"
|
|
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
|
|
)
|
|
|
|
//go:embed schema.sql
|
|
var schema string
|
|
var (
|
|
ErrInvalid = adapter.ErrInvalid
|
|
ErrStorage = adapter.ErrStorage
|
|
ErrConflict = adapter.ErrConflict
|
|
ErrCommitUnconfirmed = adapter.ErrCommitUnconfirmed
|
|
ErrIdle = adapter.ErrIdle
|
|
)
|
|
|
|
// Store 仅补充示例业务事务;发送、查询、租约由公共包负责。
|
|
type Store struct {
|
|
*adapter.Store
|
|
db *sql.DB
|
|
namespace string
|
|
}
|
|
|
|
var Open = adapter.Open
|
|
|
|
func Init(ctx context.Context, db *sql.DB) error {
|
|
if err := adapter.Init(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if _, err := db.ExecContext(ctx, schema); err != nil {
|
|
return ErrStorage
|
|
}
|
|
return nil
|
|
}
|
|
func New(db *sql.DB, namespace, queue string) (*Store, error) {
|
|
s, err := adapter.New(db, namespace, queue, adapter.Options{OnAccepted: accepted})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Store{s, db, namespace}, nil
|
|
}
|
|
func accepted(ctx context.Context, tx *sql.Tx, r adapter.Record, receipt orbit.SubmitTaskResult) error {
|
|
var existing *string
|
|
if err := tx.QueryRowContext(ctx, `SELECT task_id FROM sender_jobs WHERE namespace_id=? AND operation_id=? FOR UPDATE`, r.NamespaceID, r.OperationID).Scan(&existing); err != nil {
|
|
return ErrStorage
|
|
}
|
|
if existing != nil && *existing != receipt.Task.ID {
|
|
return ErrConflict
|
|
}
|
|
_, err := tx.ExecContext(ctx, `UPDATE sender_jobs SET task_id=?,updated_at=UTC_TIMESTAMP(6) WHERE namespace_id=? AND operation_id=?`, receipt.Task.ID, r.NamespaceID, r.OperationID)
|
|
if err != nil {
|
|
return ErrStorage
|
|
}
|
|
return nil
|
|
}
|
|
func (s *Store) Enqueue(ctx context.Context, id, source string, in orbit.SubmitTaskRequest) (bool, error) {
|
|
if !utf8.ValidString(source) || strings.TrimSpace(source) == "" || utf8.RuneCountInString(source) > 512 || strings.ContainsFunc(source, unicode.IsControl) {
|
|
return false, ErrInvalid
|
|
}
|
|
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
|
|
if err != nil {
|
|
return false, ErrStorage
|
|
}
|
|
defer tx.Rollback()
|
|
duplicate, err := s.Store.EnqueueTx(ctx, tx, id, in)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if duplicate {
|
|
var saved string
|
|
if err = tx.QueryRowContext(ctx, `SELECT source_ref FROM sender_jobs WHERE namespace_id=? AND operation_id=? FOR UPDATE`, s.namespace, id).Scan(&saved); err != nil {
|
|
return false, ErrStorage
|
|
}
|
|
if saved != source {
|
|
return false, ErrConflict
|
|
}
|
|
} else {
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO sender_jobs(namespace_id,operation_id,source_ref,created_at,updated_at) VALUES(?,?,?,UTC_TIMESTAMP(6),UTC_TIMESTAMP(6))`, s.namespace, id, source); err != nil {
|
|
return false, ErrStorage
|
|
}
|
|
}
|
|
if tx.Commit() != nil {
|
|
return false, ErrCommitUnconfirmed
|
|
}
|
|
return duplicate, nil
|
|
}
|