All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s
118 lines
4.8 KiB
Go
118 lines
4.8 KiB
Go
// 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
|
||
}
|