feat: 添加媒体资源处理的异步任务及相关逻辑
This commit is contained in:
@@ -12,9 +12,11 @@ import (
|
||||
|
||||
"quyun/v2/app/errorx"
|
||||
tenant_dto "quyun/v2/app/http/tenant/dto"
|
||||
jobs_args "quyun/v2/app/jobs/args"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/database/models"
|
||||
"quyun/v2/pkg/consts"
|
||||
provider_job "quyun/v2/providers/job"
|
||||
|
||||
pkgerrors "github.com/pkg/errors"
|
||||
"github.com/samber/lo"
|
||||
@@ -29,7 +31,30 @@ import (
|
||||
// mediaAsset 提供媒体资源上传初始化等能力(上传/处理链路会在后续里程碑补齐)。
|
||||
//
|
||||
// @provider
|
||||
type mediaAsset struct{}
|
||||
type mediaAsset struct {
|
||||
job *provider_job.Job
|
||||
}
|
||||
|
||||
func IsMediaAssetProcessJobNonRetryableError(err error) bool {
|
||||
var appErr *errorx.AppError
|
||||
if !errors.As(err, &appErr) {
|
||||
return false
|
||||
}
|
||||
switch appErr.Code {
|
||||
case errorx.CodeInvalidParameter,
|
||||
errorx.CodeRecordNotFound,
|
||||
errorx.CodeStatusConflict,
|
||||
errorx.CodePreconditionFailed,
|
||||
errorx.CodePermissionDenied:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *mediaAsset) enqueueMediaAssetProcessJob(args jobs_args.MediaAssetProcessJob) error {
|
||||
return s.job.Add(args)
|
||||
}
|
||||
|
||||
func mediaAssetTransitionAllowed(from, to consts.MediaAssetStatus) bool {
|
||||
switch from {
|
||||
@@ -218,7 +243,11 @@ func (s *mediaAsset) AdminUploadComplete(
|
||||
"asset_id": assetID,
|
||||
}).Info("services.media_asset.admin.upload_complete")
|
||||
|
||||
var out models.MediaAsset
|
||||
var (
|
||||
out models.MediaAsset
|
||||
needEnqueue bool
|
||||
enqueueArgs jobs_args.MediaAssetProcessJob
|
||||
)
|
||||
|
||||
err := models.Q.Transaction(func(tx *models.Query) error {
|
||||
tbl, query := tx.MediaAsset.QueryContext(ctx)
|
||||
@@ -241,11 +270,16 @@ func (s *mediaAsset) AdminUploadComplete(
|
||||
return errorx.ErrPreconditionFailed.WithMsg("media asset deleted")
|
||||
}
|
||||
|
||||
// 幂等:重复 upload_complete 时返回现态。
|
||||
// 幂等:重复 upload_complete 时返回现态;但只要处于 processing,就允许再次触发入队(用于“上次入队失败”的补偿重试)。
|
||||
switch m.Status {
|
||||
case consts.MediaAssetStatusProcessing, consts.MediaAssetStatusReady, consts.MediaAssetStatusFailed:
|
||||
case consts.MediaAssetStatusReady, consts.MediaAssetStatusFailed:
|
||||
out = *m
|
||||
return nil
|
||||
case consts.MediaAssetStatusProcessing:
|
||||
out = *m
|
||||
needEnqueue = true
|
||||
enqueueArgs = jobs_args.MediaAssetProcessJob{TenantID: tenantID, AssetID: assetID}
|
||||
return nil
|
||||
case consts.MediaAssetStatusUploaded:
|
||||
// allowed
|
||||
default:
|
||||
@@ -294,19 +328,32 @@ func (s *mediaAsset) AdminUploadComplete(
|
||||
m.UpdatedAt = now
|
||||
out = *m
|
||||
|
||||
// 触发异步处理(当前为 stub):后续接入队列/任务系统时在此处落任务并保持幂等。
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"tenant_id": tenantID,
|
||||
"user_id": operatorUserID,
|
||||
"asset_id": assetID,
|
||||
"status": m.Status,
|
||||
}).Info("services.media_asset.process.triggered")
|
||||
needEnqueue = true
|
||||
enqueueArgs = jobs_args.MediaAssetProcessJob{TenantID: tenantID, AssetID: assetID}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if needEnqueue {
|
||||
// 注意:River 的唯一约束会将重复入队“软跳过”,因此这里允许多次触发以补偿偶发入队失败。
|
||||
if err := s.enqueueMediaAssetProcessJob(enqueueArgs); err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"tenant_id": tenantID,
|
||||
"user_id": operatorUserID,
|
||||
"asset_id": assetID,
|
||||
}).WithError(err).Warn("services.media_asset.process.enqueue_failed")
|
||||
return nil, err
|
||||
}
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"tenant_id": tenantID,
|
||||
"user_id": operatorUserID,
|
||||
"asset_id": assetID,
|
||||
}).Info("services.media_asset.process.enqueued")
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"quyun/v2/app/commands/testx"
|
||||
"quyun/v2/app/errorx"
|
||||
tenant_dto "quyun/v2/app/http/tenant/dto"
|
||||
jobs_args "quyun/v2/app/jobs/args"
|
||||
"quyun/v2/database"
|
||||
"quyun/v2/database/models"
|
||||
"quyun/v2/pkg/consts"
|
||||
@@ -109,3 +110,49 @@ func (s *MediaAssetTestSuite) Test_AdminUploadInit_VariantAndSource() {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *MediaAssetTestSuite) Test_AdminUploadComplete_EnqueueAndProcess() {
|
||||
Convey("MediaAsset.AdminUploadComplete enqueue job and worker process", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
now := time.Now().UTC()
|
||||
tenantID := int64(1)
|
||||
userID := int64(2)
|
||||
|
||||
database.Truncate(ctx, s.DB, "river_job", models.TableNameMediaAsset)
|
||||
|
||||
asset := &models.MediaAsset{
|
||||
TenantID: tenantID,
|
||||
UserID: userID,
|
||||
Type: consts.MediaAssetTypeVideo,
|
||||
Status: consts.MediaAssetStatusUploaded,
|
||||
Provider: "test",
|
||||
Bucket: "b",
|
||||
ObjectKey: "k",
|
||||
Meta: []byte("{}"),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
So(asset.Create(ctx), ShouldBeNil)
|
||||
|
||||
Convey("首次 upload_complete:uploaded -> processing,并入队一次", func() {
|
||||
out, err := MediaAsset.AdminUploadComplete(ctx, tenantID, userID, asset.ID, nil, now)
|
||||
So(err, ShouldBeNil)
|
||||
So(out.Status, ShouldEqual, consts.MediaAssetStatusProcessing)
|
||||
|
||||
var cnt int
|
||||
err = s.DB.QueryRowContext(ctx, "SELECT COUNT(1) FROM river_job WHERE kind = $1", jobs_args.MediaAssetProcessJob{}.Kind()).Scan(&cnt)
|
||||
So(err, ShouldBeNil)
|
||||
So(cnt, ShouldEqual, 1)
|
||||
|
||||
Convey("重复 upload_complete:仍可触发入队,但不会产生重复任务", func() {
|
||||
out2, err := MediaAsset.AdminUploadComplete(ctx, tenantID, userID, asset.ID, nil, now.Add(1*time.Second))
|
||||
So(err, ShouldBeNil)
|
||||
So(out2.Status, ShouldEqual, consts.MediaAssetStatusProcessing)
|
||||
|
||||
err = s.DB.QueryRowContext(ctx, "SELECT COUNT(1) FROM river_job WHERE kind = $1", jobs_args.MediaAssetProcessJob{}.Kind()).Scan(&cnt)
|
||||
So(err, ShouldBeNil)
|
||||
So(cnt, ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,8 +30,12 @@ func Provide(opts ...opt.Option) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*mediaAsset, error) {
|
||||
obj := &mediaAsset{}
|
||||
if err := container.Container.Provide(func(
|
||||
job *provider_job.Job,
|
||||
) (*mediaAsset, error) {
|
||||
obj := &mediaAsset{
|
||||
job: job,
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user