DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,716 @@
|
||||
package eventlog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNilSQLDB 表示 SQL store 缺少可用数据库连接或事务。
|
||||
ErrNilSQLDB = errors.New("eventlog sql db is required")
|
||||
|
||||
// ErrInvalidTable 表示配置的 SQL 表名不符合安全标识符规则。
|
||||
ErrInvalidTable = errors.New("eventlog table name is invalid")
|
||||
|
||||
// ErrInvalidSQLRows 表示查询返回的 rows 为空或不满足消费契约。
|
||||
ErrInvalidSQLRows = errors.New("eventlog sql rows are invalid")
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSQLTable = "outbox_events"
|
||||
defPartMonths = 36
|
||||
defPartStartYMD = "2026-01-01"
|
||||
)
|
||||
|
||||
// SQLRows 抽象 database/sql.Rows,便于测试替换和 rows.Err 门禁覆盖。
|
||||
type SQLRows interface {
|
||||
Next() bool
|
||||
Scan(...any) error
|
||||
Close() error
|
||||
Err() error
|
||||
}
|
||||
|
||||
// SQLDB 是 eventlog SQL store 需要的最小数据库执行接口。
|
||||
type SQLDB interface {
|
||||
ExecContext(context.Context, string, ...any) (sql.Result, error)
|
||||
QueryContext(context.Context, string, ...any) (SQLRows, error)
|
||||
}
|
||||
|
||||
// SQLTransaction 是带提交和回滚能力的 SQLDB。
|
||||
type SQLTransaction interface {
|
||||
SQLDB
|
||||
Commit() error
|
||||
Rollback() error
|
||||
}
|
||||
|
||||
// SQLTransactor 能为 eventlog 追加事件开启事务。
|
||||
type SQLTransactor interface {
|
||||
BeginEventLogTx(context.Context) (SQLTransaction, error)
|
||||
}
|
||||
|
||||
// SQLDBAdapter 把 *sql.DB 适配成 eventlog SQLDB。
|
||||
type SQLDBAdapter struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
// SQLTxAdapter 把 *sql.Tx 适配成 eventlog SQLTransaction。
|
||||
type SQLTxAdapter struct {
|
||||
Tx *sql.Tx
|
||||
}
|
||||
|
||||
var (
|
||||
_ SQLDB = SQLDBAdapter{}
|
||||
_ SQLDB = SQLTxAdapter{}
|
||||
)
|
||||
|
||||
// ExecContext 在底层 *sql.DB 上执行语句。
|
||||
func (a SQLDBAdapter) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
|
||||
if a.DB == nil {
|
||||
return nil, ErrNilSQLDB
|
||||
}
|
||||
return a.DB.ExecContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
// QueryContext 在底层 *sql.DB 上查询 rows,rows 生命周期由调用方负责。
|
||||
func (a SQLDBAdapter) QueryContext(ctx context.Context, query string, args ...any) (SQLRows, error) { //nolint:rowserrcheck // 适配层只转交 rows,遍历和 Err 检查由上层消费函数负责。
|
||||
if a.DB == nil {
|
||||
return nil, ErrNilSQLDB
|
||||
}
|
||||
return a.DB.QueryContext(ctx, query, args...) //nolint:rowserrcheck // 调用方拿到 rows 后统一遍历并检查 Err。
|
||||
}
|
||||
|
||||
// BeginEventLogTx 开启 eventlog 使用的 SQL 事务。
|
||||
func (a SQLDBAdapter) BeginEventLogTx(ctx context.Context) (SQLTransaction, error) {
|
||||
if a.DB == nil {
|
||||
return nil, ErrNilSQLDB
|
||||
}
|
||||
tx, err := a.DB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return SQLTxAdapter{Tx: tx}, nil
|
||||
}
|
||||
|
||||
// ExecContext 在底层 *sql.Tx 上执行语句。
|
||||
func (a SQLTxAdapter) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
|
||||
if a.Tx == nil {
|
||||
return nil, ErrNilSQLDB
|
||||
}
|
||||
return a.Tx.ExecContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
// QueryContext 在底层 *sql.Tx 上查询 rows,rows 生命周期由调用方负责。
|
||||
func (a SQLTxAdapter) QueryContext(ctx context.Context, query string, args ...any) (SQLRows, error) { //nolint:rowserrcheck // 适配层只转交 rows,遍历和 Err 检查由上层消费函数负责。
|
||||
if a.Tx == nil {
|
||||
return nil, ErrNilSQLDB
|
||||
}
|
||||
rows, err := a.Tx.QueryContext(ctx, query, args...) //nolint:rowserrcheck // 调用方拿到 rows 后统一遍历并检查 Err。
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// Commit 提交底层事务。
|
||||
func (a SQLTxAdapter) Commit() error {
|
||||
if a.Tx == nil {
|
||||
return ErrNilSQLDB
|
||||
}
|
||||
return a.Tx.Commit()
|
||||
}
|
||||
|
||||
// Rollback 回滚底层事务。
|
||||
func (a SQLTxAdapter) Rollback() error {
|
||||
if a.Tx == nil {
|
||||
return ErrNilSQLDB
|
||||
}
|
||||
return a.Tx.Rollback()
|
||||
}
|
||||
|
||||
// SQLStoreOptions 描述 SQL eventlog 的实例名称和主表名。
|
||||
type SQLStoreOptions struct {
|
||||
Name string
|
||||
Table string
|
||||
}
|
||||
|
||||
// SQLStore 使用 SQL 表保存 eventlog/outbox 事件。
|
||||
type SQLStore struct {
|
||||
name string
|
||||
db SQLDB
|
||||
table string
|
||||
closer interface {
|
||||
Close() error
|
||||
}
|
||||
}
|
||||
|
||||
// NewSQLStore 使用抽象 SQLDB 创建 eventlog store。
|
||||
func NewSQLStore(db SQLDB, options SQLStoreOptions) (*SQLStore, error) {
|
||||
if db == nil {
|
||||
return nil, ErrNilSQLDB
|
||||
}
|
||||
table, err := normalizeTable(options.Table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := strings.TrimSpace(options.Name)
|
||||
if name == "" {
|
||||
name = "sql-eventlog"
|
||||
}
|
||||
return &SQLStore{name: name, db: db, table: table}, nil
|
||||
}
|
||||
|
||||
// NewSQLStoreFromDB 使用 *sql.DB 创建 eventlog store,并在 Close 时关闭 DB。
|
||||
func NewSQLStoreFromDB(db *sql.DB, options SQLStoreOptions) (*SQLStore, error) {
|
||||
if db == nil {
|
||||
return nil, ErrNilSQLDB
|
||||
}
|
||||
store, err := NewSQLStore(SQLDBAdapter{DB: db}, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
store.closer = db
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// NewSQLStoreFromTx 使用现有事务创建 eventlog store,不接管事务提交。
|
||||
func NewSQLStoreFromTx(tx *sql.Tx, options SQLStoreOptions) (*SQLStore, error) {
|
||||
if tx == nil {
|
||||
return nil, ErrNilSQLDB
|
||||
}
|
||||
return NewSQLStore(SQLTxAdapter{Tx: tx}, options)
|
||||
}
|
||||
|
||||
// EnsureSchema 创建 eventlog 主表、事件 ID 表和幂等键表。
|
||||
func (s *SQLStore) EnsureSchema(ctx context.Context) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return ErrNilSQLDB
|
||||
}
|
||||
for _, statement := range s.SchemaStatements() {
|
||||
if _, err := s.db.ExecContext(ctx, statement); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭由 store 接管的底层数据库连接。
|
||||
func (s *SQLStore) Close() error {
|
||||
if s == nil || s.closer == nil {
|
||||
return nil
|
||||
}
|
||||
return s.closer.Close()
|
||||
}
|
||||
|
||||
// Schema 返回完整建表 SQL 文本。
|
||||
func (s *SQLStore) Schema() string {
|
||||
return strings.Join(s.SchemaStatements(), ";\n")
|
||||
}
|
||||
|
||||
// SchemaStatements 返回 eventlog 所需的分表建表语句。
|
||||
func (s *SQLStore) SchemaStatements() []string {
|
||||
table := quoteIdentifier(s.table)
|
||||
eventIDTable := quoteIdentifier(s.eventIDTable())
|
||||
idempotencyTable := quoteIdentifier(s.idempotencyTable())
|
||||
return []string{
|
||||
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
|
||||
id VARCHAR(128) NOT NULL,
|
||||
stream VARCHAR(128) NOT NULL,
|
||||
event_type VARCHAR(128) NOT NULL,
|
||||
aggregate_id VARCHAR(128) NULL,
|
||||
idempotency_key VARCHAR(191) NULL,
|
||||
payload_json JSON NOT NULL,
|
||||
headers_json JSON NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
available_at DATETIME(6) NOT NULL,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
updated_at DATETIME(6) NOT NULL,
|
||||
published_at DATETIME(6) NULL,
|
||||
last_error TEXT NULL,
|
||||
PRIMARY KEY (id, created_at),
|
||||
KEY idx_outbox_idempotency (idempotency_key, created_at),
|
||||
KEY idx_outbox_pending (status, available_at, id, created_at),
|
||||
KEY idx_outbox_aggregate_id (aggregate_id, created_at),
|
||||
KEY idx_outbox_stream (stream, aggregate_id, created_at, id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
PARTITION BY RANGE COLUMNS(created_at) (
|
||||
%s
|
||||
)`, table, monthlyPartitions(defaultPartStart(), defPartMonths)),
|
||||
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
|
||||
id VARCHAR(128) NOT NULL,
|
||||
event_created_at DATETIME(6) NOT NULL,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_outbox_event_ids_created_at (event_created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, eventIDTable),
|
||||
fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (
|
||||
idempotency_key VARCHAR(191) NOT NULL,
|
||||
event_id VARCHAR(128) NOT NULL,
|
||||
event_created_at DATETIME(6) NOT NULL,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
PRIMARY KEY (idempotency_key),
|
||||
KEY idx_outbox_idempotency_event (event_id, event_created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, idempotencyTable),
|
||||
}
|
||||
}
|
||||
|
||||
// Append 在 SQL 中追加事件,并用辅助表保证事件 ID 与幂等键唯一。
|
||||
func (s *SQLStore) Append(ctx context.Context, event Event) (Event, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Event{}, err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return Event{}, ErrNilSQLDB
|
||||
}
|
||||
if txDB, ok := s.db.(SQLTransactor); ok {
|
||||
return s.appendInTransaction(ctx, txDB, event)
|
||||
}
|
||||
return s.appendDirect(ctx, event)
|
||||
}
|
||||
|
||||
func (s *SQLStore) appendInTransaction(ctx context.Context, txDB SQLTransactor, event Event) (Event, error) {
|
||||
tx, err := txDB.BeginEventLogTx(ctx)
|
||||
if err != nil {
|
||||
return Event{}, err
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
txStore := *s
|
||||
txStore.db = tx
|
||||
appended, err := txStore.appendDirect(ctx, event)
|
||||
if err != nil {
|
||||
return Event{}, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
if recovered, ok := s.recoverAppend(ctx, appended); ok {
|
||||
committed = true
|
||||
return recovered, nil
|
||||
}
|
||||
return Event{}, err
|
||||
}
|
||||
committed = true
|
||||
return appended, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) recoverAppend(parent context.Context, event Event) (Event, bool) {
|
||||
ctx, cancel := sqlCleanupCtx(parent)
|
||||
defer cancel()
|
||||
if event.IdempotencyKey != "" {
|
||||
existing, ok, err := s.GetByIdempotencyKey(ctx, event.IdempotencyKey)
|
||||
if err == nil && ok && sameIdempotentEvent(existing, event) {
|
||||
return existing, true
|
||||
}
|
||||
}
|
||||
existing, ok, err := s.Get(ctx, event.ID)
|
||||
if err == nil && ok && sameIdempotentEvent(existing, event) {
|
||||
return existing, true
|
||||
}
|
||||
return Event{}, false
|
||||
}
|
||||
|
||||
func (s *SQLStore) appendDirect(ctx context.Context, event Event) (Event, error) {
|
||||
// Append 先查幂等键和事件 ID,再用辅助表保留 ID;主表按 created_at 分区,不能单靠分区主键保证 ID 全局唯一。
|
||||
if event.IdempotencyKey != "" {
|
||||
if existing, ok, err := s.GetByIdempotencyKey(ctx, event.IdempotencyKey); err != nil {
|
||||
return Event{}, err
|
||||
} else if ok {
|
||||
if !sameIdempotentEvent(existing, event) {
|
||||
return Event{}, ErrIdempotencyConflict
|
||||
}
|
||||
return existing, nil
|
||||
}
|
||||
}
|
||||
if _, ok, err := s.Get(ctx, event.ID); err != nil {
|
||||
return Event{}, err
|
||||
} else if ok {
|
||||
return Event{}, ErrEventExists
|
||||
}
|
||||
reservedID := false
|
||||
if err := s.reserveEventID(ctx, event); err != nil {
|
||||
if _, ok, getErr := s.Get(ctx, event.ID); getErr != nil {
|
||||
return Event{}, getErr
|
||||
} else if ok {
|
||||
return Event{}, ErrEventExists
|
||||
}
|
||||
return Event{}, err
|
||||
}
|
||||
reservedID = true
|
||||
reservedIdempotency := false
|
||||
if event.IdempotencyKey != "" {
|
||||
if err := s.reserveIdempotency(ctx, event); err != nil {
|
||||
if reservedID {
|
||||
s.releaseEventIDSafe(ctx, event)
|
||||
}
|
||||
if existing, ok, getErr := s.GetByIdempotencyKey(ctx, event.IdempotencyKey); getErr != nil {
|
||||
return Event{}, getErr
|
||||
} else if ok {
|
||||
if !sameIdempotentEvent(existing, event) {
|
||||
return Event{}, ErrIdempotencyConflict
|
||||
}
|
||||
return existing, nil
|
||||
}
|
||||
return Event{}, err
|
||||
}
|
||||
reservedIdempotency = true
|
||||
}
|
||||
payload := string(event.Payload)
|
||||
headers, err := nullableJSON(event.Headers)
|
||||
if err != nil {
|
||||
if reservedID {
|
||||
s.releaseEventIDSafe(ctx, event)
|
||||
}
|
||||
if reservedIdempotency {
|
||||
s.releaseIdemSafe(ctx, event)
|
||||
}
|
||||
return Event{}, err
|
||||
}
|
||||
_, err = s.db.ExecContext(ctx,
|
||||
"INSERT INTO "+quoteIdentifier(s.table)+" (id, stream, event_type, aggregate_id, idempotency_key, payload_json, headers_json, status, attempts, available_at, created_at, updated_at, published_at, last_error) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
event.ID,
|
||||
event.Stream,
|
||||
event.Type,
|
||||
nullableString(event.AggregateID),
|
||||
nullableString(event.IdempotencyKey),
|
||||
payload,
|
||||
headers,
|
||||
event.Status,
|
||||
event.Attempts,
|
||||
event.AvailableAt,
|
||||
event.CreatedAt,
|
||||
event.UpdatedAt,
|
||||
nullableTime(event.PublishedAt),
|
||||
nullableString(event.LastError),
|
||||
)
|
||||
if err != nil {
|
||||
if reservedID {
|
||||
s.releaseEventIDSafe(ctx, event)
|
||||
}
|
||||
if reservedIdempotency {
|
||||
s.releaseIdemSafe(ctx, event)
|
||||
}
|
||||
if event.IdempotencyKey != "" {
|
||||
if existing, ok, getErr := s.GetByIdempotencyKey(ctx, event.IdempotencyKey); getErr != nil {
|
||||
return Event{}, getErr
|
||||
} else if ok {
|
||||
if !sameIdempotentEvent(existing, event) {
|
||||
return Event{}, ErrIdempotencyConflict
|
||||
}
|
||||
return existing, nil
|
||||
}
|
||||
}
|
||||
return Event{}, err
|
||||
}
|
||||
return cloneEvent(event), nil
|
||||
}
|
||||
|
||||
// Get 按事件 ID 查询 SQL 中的事件。
|
||||
func (s *SQLStore) Get(ctx context.Context, id string) (Event, bool, error) {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
return Event{}, false, nil
|
||||
}
|
||||
return s.queryOne(ctx, "SELECT "+eventColumns()+" FROM "+quoteIdentifier(s.table)+" WHERE id = ? LIMIT 1", id)
|
||||
}
|
||||
|
||||
// GetByIdempotencyKey 按幂等键查询 SQL 中的事件。
|
||||
func (s *SQLStore) GetByIdempotencyKey(ctx context.Context, key string) (Event, bool, error) {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
return Event{}, false, nil
|
||||
}
|
||||
return s.queryOne(ctx,
|
||||
"SELECT "+prefixedEventColumns("e")+" FROM "+quoteIdentifier(s.table)+" e JOIN "+quoteIdentifier(s.idempotencyTable())+" i ON e.id = i.event_id AND e.created_at = i.event_created_at WHERE i.idempotency_key = ? LIMIT 1",
|
||||
key,
|
||||
)
|
||||
}
|
||||
|
||||
// ListByStreamAggregate 按事件流和聚合 ID 查询最近事件。
|
||||
func (s *SQLStore) ListByStreamAggregate(ctx context.Context, stream, aggregateID string, limit int) ([]Event, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return nil, ErrNilSQLDB
|
||||
}
|
||||
stream = strings.TrimSpace(stream)
|
||||
aggregateID = strings.TrimSpace(aggregateID)
|
||||
if stream == "" {
|
||||
return nil, fmt.Errorf("%w: stream is required", ErrInvalidEvent)
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = defaultPublishLimit
|
||||
}
|
||||
query := "SELECT " + eventColumns() + " FROM " + quoteIdentifier(s.table) + " WHERE stream = ?"
|
||||
args := []any{stream}
|
||||
if aggregateID == "" {
|
||||
query += " AND aggregate_id IS NULL"
|
||||
} else {
|
||||
query += " AND aggregate_id = ?"
|
||||
args = append(args, aggregateID)
|
||||
}
|
||||
query += " ORDER BY created_at DESC, id DESC LIMIT ?"
|
||||
args = append(args, limit)
|
||||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return scanEvents(rows)
|
||||
}
|
||||
|
||||
// ClaimPending 领取一批 SQL 中到期的待发布事件。
|
||||
func (s *SQLStore) ClaimPending(ctx context.Context, limit int, now time.Time, claimTimeout time.Duration) ([]Event, error) {
|
||||
return s.claimPending(ctx, limit, now, claimTimeout, nil)
|
||||
}
|
||||
|
||||
// ClaimPendingExcept 领取到期事件时排除指定事件流。
|
||||
func (s *SQLStore) ClaimPendingExcept(ctx context.Context, limit int, now time.Time, claimTimeout time.Duration, excludeStreams []string) ([]Event, error) {
|
||||
return s.claimPending(ctx, limit, now, claimTimeout, normalizeStreamList(excludeStreams))
|
||||
}
|
||||
|
||||
func (s *SQLStore) claimPending(ctx context.Context, limit int, now time.Time, claimTimeout time.Duration, excludeStreams []string) ([]Event, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return nil, ErrNilSQLDB
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = defaultPublishLimit
|
||||
}
|
||||
now = now.UTC()
|
||||
if claimTimeout <= 0 {
|
||||
claimTimeout = defaultClaimTimeout
|
||||
}
|
||||
claimUntil := now.Add(claimTimeout)
|
||||
// SQL claim 通过状态和 available_at 条件竞争领取,领取成功后 available_at 变成本次 claim deadline。
|
||||
events, err := s.fetchClaimable(ctx, limit, now, excludeStreams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claimed := make([]Event, 0, len(events))
|
||||
for _, event := range events {
|
||||
result, err := s.db.ExecContext(ctx,
|
||||
"UPDATE "+quoteIdentifier(s.table)+" SET status = ?, available_at = ?, updated_at = ? WHERE id = ? AND status IN (?, ?, ?) AND available_at <= ?",
|
||||
StatusProcessing,
|
||||
claimUntil,
|
||||
now,
|
||||
event.ID,
|
||||
StatusPending,
|
||||
StatusFailed,
|
||||
StatusProcessing,
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if affected == 0 {
|
||||
continue
|
||||
}
|
||||
event.Status = StatusProcessing
|
||||
event.AvailableAt = claimUntil
|
||||
event.UpdatedAt = now
|
||||
claimed = append(claimed, cloneEvent(event))
|
||||
}
|
||||
return claimed, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) fetchClaimable(ctx context.Context, limit int, now time.Time, excludeStreams []string) ([]Event, error) {
|
||||
query := "SELECT " + eventColumns() + " FROM " + quoteIdentifier(s.table) + " WHERE status IN (?, ?, ?) AND available_at <= ?"
|
||||
args := []any{
|
||||
StatusPending,
|
||||
StatusFailed,
|
||||
StatusProcessing,
|
||||
now,
|
||||
}
|
||||
if len(excludeStreams) > 0 {
|
||||
query += " AND stream NOT IN (" + placeholders(len(excludeStreams)) + ")"
|
||||
for _, stream := range excludeStreams {
|
||||
args = append(args, stream)
|
||||
}
|
||||
}
|
||||
query += " ORDER BY available_at ASC, id ASC LIMIT ?"
|
||||
args = append(args, limit)
|
||||
rows, err := s.db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return scanEvents(rows)
|
||||
}
|
||||
|
||||
// MarkPublished 将当前 claim 持有的 SQL 事件推进为已发布。
|
||||
func (s *SQLStore) MarkPublished(ctx context.Context, id string, publishedAt time.Time, claimDeadline time.Time) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return ErrNilSQLDB
|
||||
}
|
||||
publishedAt = publishedAt.UTC()
|
||||
result, err := s.execClaimUpdate(ctx, claimDeadline,
|
||||
"UPDATE "+quoteIdentifier(s.table)+" SET status = ?, published_at = ?, updated_at = ?, last_error = NULL WHERE id = ?",
|
||||
"UPDATE "+quoteIdentifier(s.table)+" SET status = ?, published_at = ?, updated_at = ?, last_error = NULL WHERE id = ? AND status = ? AND available_at = ?",
|
||||
StatusPublished,
|
||||
publishedAt,
|
||||
publishedAt,
|
||||
id,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return checkClaimAffected(result, claimDeadline)
|
||||
}
|
||||
|
||||
// MarkFailed 将当前 claim 持有的 SQL 事件推进为失败并设置下次重试时间。
|
||||
func (s *SQLStore) MarkFailed(ctx context.Context, id string, message string, nextAttempt time.Time, updatedAt time.Time, claimDeadline time.Time) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return ErrNilSQLDB
|
||||
}
|
||||
result, err := s.execClaimUpdate(ctx, claimDeadline,
|
||||
"UPDATE "+quoteIdentifier(s.table)+" SET status = ?, attempts = attempts + 1, available_at = ?, updated_at = ?, last_error = ? WHERE id = ?",
|
||||
"UPDATE "+quoteIdentifier(s.table)+" SET status = ?, attempts = attempts + 1, available_at = ?, updated_at = ?, last_error = ? WHERE id = ? AND status = ? AND available_at = ?",
|
||||
StatusFailed,
|
||||
nextAttempt.UTC(),
|
||||
updatedAt.UTC(),
|
||||
message,
|
||||
id,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return checkClaimAffected(result, claimDeadline)
|
||||
}
|
||||
|
||||
// MarkDeadLetter 将当前 claim 持有的 SQL 事件推进为死信。
|
||||
func (s *SQLStore) MarkDeadLetter(ctx context.Context, id string, message string, updatedAt time.Time, claimDeadline time.Time) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return ErrNilSQLDB
|
||||
}
|
||||
updatedAt = updatedAt.UTC()
|
||||
result, err := s.execClaimUpdate(ctx, claimDeadline,
|
||||
"UPDATE "+quoteIdentifier(s.table)+" SET status = ?, attempts = attempts + 1, available_at = ?, updated_at = ?, last_error = ? WHERE id = ?",
|
||||
"UPDATE "+quoteIdentifier(s.table)+" SET status = ?, attempts = attempts + 1, available_at = ?, updated_at = ?, last_error = ? WHERE id = ? AND status = ? AND available_at = ?",
|
||||
StatusDeadLetter,
|
||||
updatedAt,
|
||||
updatedAt,
|
||||
message,
|
||||
id,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return checkClaimAffected(result, claimDeadline)
|
||||
}
|
||||
|
||||
// ListDeadLetters 查询 SQL 中的死信事件。
|
||||
func (s *SQLStore) ListDeadLetters(ctx context.Context, limit int) ([]Event, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return nil, ErrNilSQLDB
|
||||
}
|
||||
if limit < 0 {
|
||||
return nil, ErrInvalidLimit
|
||||
}
|
||||
if limit == 0 {
|
||||
limit = defDeadLetterLimit
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
"SELECT "+eventColumns()+" FROM "+quoteIdentifier(s.table)+" WHERE status = ? ORDER BY updated_at ASC, id ASC LIMIT ?",
|
||||
StatusDeadLetter,
|
||||
limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return scanEvents(rows)
|
||||
}
|
||||
|
||||
// RequeueDeadLetter 把 SQL 中的死信事件重新放回待发布队列。
|
||||
func (s *SQLStore) RequeueDeadLetter(ctx context.Context, id string, availableAt time.Time, updatedAt time.Time, preserveAttempts bool) error {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return ErrNilSQLDB
|
||||
}
|
||||
availableAt = availableAt.UTC()
|
||||
updatedAt = updatedAt.UTC()
|
||||
var (
|
||||
result sql.Result
|
||||
err error
|
||||
)
|
||||
if preserveAttempts {
|
||||
result, err = s.db.ExecContext(ctx,
|
||||
"UPDATE "+quoteIdentifier(s.table)+" SET status = ?, available_at = ?, updated_at = ?, published_at = NULL, last_error = NULL WHERE id = ? AND status = ?",
|
||||
StatusPending,
|
||||
availableAt,
|
||||
updatedAt,
|
||||
id,
|
||||
StatusDeadLetter,
|
||||
)
|
||||
} else {
|
||||
result, err = s.db.ExecContext(ctx,
|
||||
"UPDATE "+quoteIdentifier(s.table)+" SET status = ?, attempts = 0, available_at = ?, updated_at = ?, published_at = NULL, last_error = NULL WHERE id = ? AND status = ?",
|
||||
StatusPending,
|
||||
availableAt,
|
||||
updatedAt,
|
||||
id,
|
||||
StatusDeadLetter,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return checkAffected(result)
|
||||
}
|
||||
|
||||
// Snapshot 汇总 SQL store 当前各状态数量和最早到期时间。
|
||||
func (s *SQLStore) Snapshot(ctx context.Context) (Snapshot, error) {
|
||||
if err := ctxErr(ctx); err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
if s == nil || s.db == nil {
|
||||
return Snapshot{}, ErrNilSQLDB
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx, "SELECT "+eventColumns()+" FROM "+quoteIdentifier(s.table)+" ORDER BY created_at ASC, id ASC")
|
||||
if err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
events, err := scanEvents(rows)
|
||||
if err != nil {
|
||||
return Snapshot{}, err
|
||||
}
|
||||
snapshot := Snapshot{Name: s.name, Total: len(events), ByStatus: make(map[string]int)}
|
||||
for _, event := range events {
|
||||
snapshot.ByStatus[event.Status]++
|
||||
if isDueStatus(event.Status) && (snapshot.OldestDue.IsZero() || event.AvailableAt.Before(snapshot.OldestDue)) {
|
||||
snapshot.OldestDue = event.AvailableAt
|
||||
}
|
||||
}
|
||||
if len(snapshot.ByStatus) == 0 {
|
||||
snapshot.ByStatus = nil
|
||||
}
|
||||
return snapshot, nil
|
||||
}
|
||||
Reference in new issue
Block a user