实现七月迭代公共技术基础
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s
This commit is contained in:
117
internal/infrastructure/asynctask/store.go
Normal file
117
internal/infrastructure/asynctask/store.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// Package asynctask 提供业务自有任务表复用的 PostgreSQL 条件更新 Adapter。
|
||||
package asynctask
|
||||
|
||||
import (
|
||||
"context"
|
||||
stderrors "errors"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
contract "github.com/break/junhong_cmp_fiber/pkg/asynctask"
|
||||
)
|
||||
|
||||
var identifierPattern = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
|
||||
|
||||
// Definition 描述业务自有任务表如何映射统一字段。
|
||||
type Definition struct {
|
||||
Table string
|
||||
IDColumn string
|
||||
StatusColumn string
|
||||
LeaseOwnerColumn string
|
||||
LeaseExpiresColumn string
|
||||
TotalColumn string
|
||||
SuccessColumn string
|
||||
FailedColumn string
|
||||
ProgressColumn string
|
||||
ErrorCodeColumn string
|
||||
ErrorSummaryColumn string
|
||||
StartedAtColumn string
|
||||
CompletedAtColumn string
|
||||
UpdatedAtColumn string
|
||||
}
|
||||
|
||||
// Store 对一个已注册的业务任务表执行统一条件更新。
|
||||
type Store struct {
|
||||
db *gorm.DB
|
||||
definition Definition
|
||||
}
|
||||
|
||||
// NewStore 创建业务任务表 Adapter,不创建或迁移任何万能任务表。
|
||||
func NewStore(db *gorm.DB, definition Definition) (*Store, error) {
|
||||
columns := []string{
|
||||
definition.Table, definition.IDColumn, definition.StatusColumn, definition.LeaseOwnerColumn,
|
||||
definition.LeaseExpiresColumn, definition.TotalColumn, definition.SuccessColumn,
|
||||
definition.FailedColumn, definition.ProgressColumn, definition.ErrorCodeColumn,
|
||||
definition.ErrorSummaryColumn, definition.StartedAtColumn, definition.CompletedAtColumn, definition.UpdatedAtColumn,
|
||||
}
|
||||
for _, identifier := range columns {
|
||||
if !identifierPattern.MatchString(identifier) {
|
||||
return nil, stderrors.New("异步任务表或字段标识不合法")
|
||||
}
|
||||
}
|
||||
return &Store{db: db, definition: definition}, nil
|
||||
}
|
||||
|
||||
// Claim 使用预期状态和过期租约条件领取任务。
|
||||
func (s *Store) Claim(ctx context.Context, id any, owner string, now time.Time, duration time.Duration) (bool, error) {
|
||||
if owner == "" || duration <= 0 {
|
||||
return false, stderrors.New("任务租约所有者和时长不能为空")
|
||||
}
|
||||
d := s.definition
|
||||
result := s.db.WithContext(ctx).Table(d.Table).
|
||||
Where(d.IDColumn+" = ? AND ("+d.StatusColumn+" = ? OR ("+d.StatusColumn+" = ? AND "+d.LeaseExpiresColumn+" <= ?))",
|
||||
id, contract.StatusPending, contract.StatusProcessing, now).
|
||||
Updates(map[string]any{
|
||||
d.StatusColumn: contract.StatusProcessing, d.LeaseOwnerColumn: owner,
|
||||
d.LeaseExpiresColumn: now.Add(duration), d.StartedAtColumn: gorm.Expr("COALESCE("+d.StartedAtColumn+", ?)", now),
|
||||
d.UpdatedAtColumn: now,
|
||||
})
|
||||
return result.RowsAffected == 1, result.Error
|
||||
}
|
||||
|
||||
// Renew 只允许当前所有者在租约有效时续期处理中任务。
|
||||
func (s *Store) Renew(ctx context.Context, id any, owner string, now time.Time, duration time.Duration) (bool, error) {
|
||||
if owner == "" || duration <= 0 {
|
||||
return false, stderrors.New("任务租约所有者和时长不能为空")
|
||||
}
|
||||
d := s.definition
|
||||
updated := s.db.WithContext(ctx).Table(d.Table).
|
||||
Where(d.IDColumn+" = ? AND "+d.StatusColumn+" = ? AND "+d.LeaseOwnerColumn+" = ? AND "+d.LeaseExpiresColumn+" > ?",
|
||||
id, contract.StatusProcessing, owner, now).
|
||||
Updates(map[string]any{d.LeaseExpiresColumn: now.Add(duration), d.UpdatedAtColumn: now})
|
||||
return updated.RowsAffected == 1, updated.Error
|
||||
}
|
||||
|
||||
// Finish 只允许有效租约所有者把处理中任务推进到统一终态。
|
||||
func (s *Store) Finish(ctx context.Context, id any, owner string, result contract.TerminalResult, now time.Time) (bool, error) {
|
||||
projection, err := contract.NewTerminalProjection(result)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
d := s.definition
|
||||
updated := s.db.WithContext(ctx).Table(d.Table).
|
||||
Where(d.IDColumn+" = ? AND "+d.StatusColumn+" = ? AND "+d.LeaseOwnerColumn+" = ? AND "+d.LeaseExpiresColumn+" > ?",
|
||||
id, contract.StatusProcessing, owner, now).
|
||||
Updates(map[string]any{
|
||||
d.StatusColumn: projection.Status, d.TotalColumn: projection.TotalCount,
|
||||
d.SuccessColumn: projection.SuccessCount, d.FailedColumn: projection.FailedCount,
|
||||
d.ProgressColumn: projection.Progress, d.ErrorCodeColumn: projection.ErrorCode,
|
||||
d.ErrorSummaryColumn: projection.ErrorSummary, d.CompletedAtColumn: now,
|
||||
d.LeaseOwnerColumn: nil, d.LeaseExpiresColumn: nil, d.UpdatedAtColumn: now,
|
||||
})
|
||||
return updated.RowsAffected == 1, updated.Error
|
||||
}
|
||||
|
||||
// Cancel 只允许业务明确支持时从待处理或处理中进入已取消。
|
||||
func (s *Store) Cancel(ctx context.Context, id any, now time.Time) (bool, error) {
|
||||
d := s.definition
|
||||
updated := s.db.WithContext(ctx).Table(d.Table).
|
||||
Where(d.IDColumn+" = ? AND "+d.StatusColumn+" IN ?", id, []int{contract.StatusPending, contract.StatusProcessing}).
|
||||
Updates(map[string]any{
|
||||
d.StatusColumn: contract.StatusCancelled, d.ProgressColumn: 100,
|
||||
d.CompletedAtColumn: now, d.LeaseOwnerColumn: nil, d.LeaseExpiresColumn: nil, d.UpdatedAtColumn: now,
|
||||
})
|
||||
return updated.RowsAffected == 1, updated.Error
|
||||
}
|
||||
Reference in New Issue
Block a user